Nabeel Ahmed
Nabeel Ahmed

Reputation: 242

How can I make sticky headers in RecyclerView? (with or without external lib)

I'm using room db and I've a table from where I get a list of LiveData. In that table there is a column of Date where I store current date. Current date is selected by default, but user can also change the date when inserting data in the database.

I want to show this data in a recyclerview in this manner Image https://i.sstatic.net/KBASl.jpg

I want to section this data according to the month and year as header and all entries of that month year below it.

For example, user inserted data in October 2019, I want this "October 2019" as a header in recyclerview and all entries of this month below it. Just like this all months entries should be shown with same manner as every next month becomes header and the entries of that month below it.

I've tried to achieve this by doing

if (!thisDate.equals(dBDate))
        {
            holder.transMonthWrapper.setVisibility(View.VISIBLE);

            if (IEList.getType().equalsIgnoreCase("income"))
            {
                String amount = ""+IEList.getAmount();
                holder.tvTransAmount.setText(amount);
                holder.tvTransAmount.setTextColor(Color.GREEN);

                holder.tvTransCategory.setText(IEList.getCategory());
                holder.tvTransCategory.setTextColor(Color.GREEN);
            }
            else
            {
                String amount = ""+IEList.getAmount();
                holder.tvTransAmount.setText(amount);
                holder.tvTransAmount.setTextColor(Color.RED);

                holder.tvTransCategory.setText(IEList.getCategory());
                holder.tvTransCategory.setTextColor(Color.RED);
            }

            thisDate = dBDate;

            holder.tvTransMonth.setText(thisDate);
        }

        else
        {
            holder.transMonthWrapper.setVisibility(View.GONE);

            if (IEList.getType().equalsIgnoreCase("income"))
            {
                String amount = ""+IEList.getAmount();
                holder.tvTransAmount.setText(amount);
                holder.tvTransAmount.setTextColor(Color.GREEN);

                holder.tvTransCategory.setText(IEList.getCategory());
                holder.tvTransCategory.setTextColor(Color.GREEN);
            }
            else
            {
                String amount = ""+IEList.getAmount();
                holder.tvTransAmount.setText(amount);
                holder.tvTransAmount.setTextColor(Color.RED);

                holder.tvTransCategory.setText(IEList.getCategory());
                holder.tvTransCategory.setTextColor(Color.RED);
            }
        }

But the problem in this code is that when user change the month from settings and put some entries into database and that year's month entries are already present in the recyclerview. It creates another header of that existing month in recyclerview. But I want this to put those entries in existing month header, not to create new header of that month.

What will be the best approach to achieve this without using external libraries, because I don't want to be dependent on external libraries in this case.

I'm fairly new in programming.

Updated

In activity

 public void getTransactionData()
{
    adapter = new TransactionAdapter();
    recyclerView.setAdapter(adapter);

    incomeExpenseModel = ViewModelProviders.of(AllTransaction.this).get(IncomeExpenseViewModel.class);
    incomeExpenseModel.getIncomeExpenseData().observe(this, new Observer<List<IncomeExpense>>() {
        @Override
        public void onChanged(List<IncomeExpense> incomeExpenses) {

            adapter.setIncomeExpenseList(incomeExpenses);
        }
    });

In recyclerAdapter

public void onBindViewHolder(@NonNull TransactionViewHolder holder, int position) {

    IncomeExpense IEList = incomeExpenseList.get(position);

    preferences = context.getSharedPreferences(settingPref, Context.MODE_PRIVATE);
    String dateFormat = preferences.getString("Date_Format", "MM.dd.yy");

    int lastIndex = incomeExpenseList.size() - 1;
    IncomeExpense IELastIndex = incomeExpenseList.get(lastIndex);

    String dateFrmDb= IELastIndex.getDate();
    DateFormat df=new SimpleDateFormat(dateFormat);
    Date d;

    try {
        d = df.parse(dateFrmDb);
        df=new SimpleDateFormat("MMMM yyyy");
        if (d != null) {
            dBDate = df.format(d);
        }
    } catch (ParseException e) {

        Toast.makeText(context, "Error" +e, Toast.LENGTH_SHORT).show();
    }

    if (!thisDate.equals(dBDate))
    {
        holder.transMonthWrapper.setVisibility(View.VISIBLE);

        if (IEList.getType().equalsIgnoreCase("income"))
        {
            String amount = ""+IEList.getAmount();
            holder.tvTransAmount.setText(amount);
            holder.tvTransAmount.setTextColor(Color.GREEN);

            holder.tvTransCategory.setText(IEList.getCategory());
            holder.tvTransCategory.setTextColor(Color.GREEN);
        }
        else
        {
            String amount = ""+IEList.getAmount();
            holder.tvTransAmount.setText(amount);
            holder.tvTransAmount.setTextColor(Color.RED);

            holder.tvTransCategory.setText(IEList.getCategory());
            holder.tvTransCategory.setTextColor(Color.RED);
        }

        thisDate = dBDate;

        holder.tvTransMonth.setText(thisDate);
    }

    else
    {
        holder.transMonthWrapper.setVisibility(View.GONE);

        if (IEList.getType().equalsIgnoreCase("income"))
        {
            String amount = ""+IEList.getAmount();
            holder.tvTransAmount.setText(amount);
            holder.tvTransAmount.setTextColor(Color.GREEN);

            holder.tvTransCategory.setText(IEList.getCategory());
            holder.tvTransCategory.setTextColor(Color.GREEN);
        }
        else
        {
            String amount = ""+IEList.getAmount();
            holder.tvTransAmount.setText(amount);
            holder.tvTransAmount.setTextColor(Color.RED);

            holder.tvTransCategory.setText(IEList.getCategory());
            holder.tvTransCategory.setTextColor(Color.RED);
        }
    }

}

@Override
public int getItemCount() {
    return incomeExpenseList.size();
}

public void setIncomeExpenseList(List<IncomeExpense> incomeExpenseList)
{
    this.incomeExpenseList = incomeExpenseList;
    notifyDataSetChanged();
}

Upvotes: 0

Views: 6619

Answers (3)

dani3264
dani3264

Reputation: 11

  1. First in you setIncomeExpenseList function clear your previous list.
  2. Then reAssign new list that contains full data from DB.
  3. Change notifyDataSetChanged() to notify() or notifyAll().

    public void setIncomeExpenseList(List<IncomeExpense> incomeExpenseList) { this.incomeExpenceList.clear(); this.incomeExpenseList = incomeExpenseList; notify(); }

Upvotes: 1

kam1234
kam1234

Reputation: 572

Parent recyclerview

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".fragments.PhotoFragment">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_photo"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        android:clipToPadding="false"
        android:paddingBottom="170dp">

    </androidx.recyclerview.widget.RecyclerView>

</FrameLayout>

Its adapter

public class PhotoAdapter extends RecyclerView.Adapter<PhotoAdapter.ViewHolder> {

    private static final String TAG = "PhotoAdapter";
    private Map<String, List<Photo>> photoMap;
    private Context context;
    private PhotoCategoryAdapter photoCategoryAdapter;
    private List<String> keysList;

    public PhotoAdapter(Map<String, List<Photo>> photoMap, Context context, List<String> keysList) {
        this.photoMap = photoMap;
        this.context = context;
        this.keysList = keysList;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ViewHolder(LayoutInflater.from(parent.getContext())
                .inflate(R.layout.row_photo, parent, false));
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        String date  = keysList.get(position);
        holder.lblTakenDate.setText(date);

        List<Photo> photoList = photoMap.get(date);
        String size = ("( "+ photoList.size() + " )");
        holder.lblCountPhotos.setText(size);

        setRecyclerView(holder, context, photoList);

    }

    @Override
    public int getItemCount() {
        return photoMap.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {

        @BindView(R.id.rv_photo_category) RecyclerView recyclerView;
        @BindView(R.id.lbl_taken_date_photo) TextView lblTakenDate;
        @BindView(R.id.lbl_count_images_photo) TextView lblCountPhotos;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
        }
    }
}

Reyclerview inside recyclerview

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="5dp">

    <TextView
        android:id="@+id/lbl_taken_date_photo"
        android:textColor="@android:color/black"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="5dp"
        android:textSize="14dp"
        android:hint="23-Aug-2018" />

    <TextView
        android:id="@+id/lbl_count_images_photo"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="5dp"
        android:layout_toRightOf="@id/lbl_taken_date_photo"
        android:hint="(2)"
        android:textSize="12sp" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_photo_category"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/lbl_taken_date_photo"
        android:layout_marginTop="10dp"
        android:background="@color/light_grey" />

</RelativeLayout>

Its adapter

public class PhotoCategoryAdapter extends RecyclerView.Adapter<PhotoCategoryAdapter.ViewHolder> {

    private static final String TAG = "PhotoCategoryAdapter";
    private List<Photo> photoList;
    private Context context;

    public PhotoCategoryAdapter(List<Photo> photoList, Context context) {
        this.photoList = photoList;
        this.context = context;

    }

    @NonNull
    @Override
    public PhotoCategoryAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.row_category_photo, parent, false));
    }


    @Override
    public void onBindViewHolder(@NonNull PhotoCategoryAdapter.ViewHolder holder, int position) {
        Photo photo = photoList.get(position);

        RequestOptions myOptions = new RequestOptions() .format(DecodeFormat.PREFER_ARGB_8888).
                fitCenter().override(100, 100).placeholderOf(R.drawable.ic_image);

        Glide.with(context)
                .applyDefaultRequestOptions(myOptions)
                .asBitmap()
                .load(photo.getImage())
                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
                .into(holder.imgVImages);
    }

    @Override
    public int getItemCount() {
        return photoList.size();
    }

    public class ViewHolder extends RecyclerView.ViewHolder {
        @BindView(R.id.imgv_images_category_photo)
        ImageView imgVImages;
        @BindView(R.id.imgv_selected_icon_photo) ImageView imgVSelectedPhoto;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
        }
    }
}

Row of inner recyclerview

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_margin="5dp"
    android:layout_width="80dp"
    android:layout_height="80dp">

    <ImageView
        android:id="@+id/imgv_images_category_photo"
        android:scaleType="fitXY"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <ImageView
        android:id="@+id/imgv_selected_icon_photo"
        android:visibility="gone"
        android:layout_width="15dp"
        android:layout_height="15dp"
        android:layout_margin="10dp"
        android:src="@drawable/ic_select"
        android:layout_alignParentEnd="true"/>

</RelativeLayout>

Activity or fragment from where you will send data to adapter

public class PhotoFragment extends Fragment {

    @BindView(R.id.rv_photo) RecyclerView recyclerView;

    private PhotoAdapter photoAdapter;
    private ArrayList<Photo> photoList;
    private ArrayList<String> keyList;
    private Map<String,List<Photo>> map;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_photo, container, false);
        ButterKnife.bind(this, view);

        init();
        setRecyclerViewAdapter();
        new PhotoAsync(getContext()).execute();

        return view;
    }

    private void init(){
        map = new HashMap<>();
        keyList = new ArrayList<>();
        photoList = new ArrayList<>();
    }

    //set layout manager to recyclerView
    private void setRecyclerViewAdapter() {
        recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
    }

    //get list of images
    @RequiresApi(api = Build.VERSION_CODES.Q)
    private List<Photo> getAllImages(){
        Uri u = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        String[] projection = {MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.DATE_TAKEN};
        Cursor c = null;
        ArrayList<Photo> photoList = new ArrayList<>();

        if (u != null) {
            c = getContext().getContentResolver().query(u, projection, null, null, null); }

        if ((c != null) && (c.moveToFirst())) {
            do {
                Photo photo = new Photo();
                String path = c.getString(c.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATA));

                String takenDate = c.getString(c.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
                long millisecond = Long.parseLong(takenDate);
                String date = DateFormat.format("dd-MMM-yyyy", new Date(millisecond)).toString();

                try{
                    photo.setImage(path);
                    photo.setDate(date);
                    photoList.add(photo);
                }
                catch(Exception e)
                { }

            }
            while (c.moveToNext());
        }
        //reverse photoList
        Collections.reverse(photoList);

        return photoList;
    }

    public class PhotoAsync extends AsyncTask<Void, Void,  Void> {
        private Context context;

        public PhotoAsync(Context context) {
            this.context = context;
        }

        @RequiresApi(api = Build.VERSION_CODES.Q)
        @Override
        protected Void doInBackground(Void... params) {
            getPhotoList();
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            photoAdapter = new PhotoAdapter(map, context, keyList);
            recyclerView.setAdapter(photoAdapter);
            photoAdapter.notifyDataSetChanged();
        }

        @Override
        protected void onPreExecute() {
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    private void getPhotoList(){
        photoList.addAll(getAllImages());

        for (Photo string : photoList) {
            if (!keyList.contains(string.getDate()))
                keyList.add(string.getDate());
            else;
        }

        for (String s : keyList){
            ArrayList<Photo> photos = new ArrayList<>();
            for (Photo s1 : photoList) {
                if (s1.getDate().equals(s))
                    photos.add(s1);
                else;
            }
            map.put(s, photos);
        }
    }
}
Here is the logic which you have to apply 
1. Find all the common dates from the data list 
   for example you data list is : 
   List<Model> dataList = new ArrayList<>();
   dataList.add(new Model("23/10/19"));
   dataList.add(new Model("23/10/19"));
   dataList.add(new Model("23/09/19"));
   dataList.add(new Model("23/10/19"));
   dataList.add(new Model("27/09/19"));
   dataList.add(new Model("23/10/19"));

   List<String> commonList = new ArrayList<>();
   for(Model m : dataList){
       if(!commonList.contains(model.getDate()))
             commonList.add(m.getDate());
       else
           Log.d("Dates", commonList);
   }

   Above function will help in getting all common dates

   //Here map store date with data which has common date
   Map<String, List<Model>> map = new HashMap<>(); 
   List<Model> objectsofCommonDate = new ArrayList();

   for(String date: commonList){
       objectsofCommonDate.clear();
       for(Model model : dataList){
           if(model.getData.contains(date))
               objectsofCommonDate.add(model);
           else
             \\do nothing
       }
       map.put(data, objectsOfCommonDate);
   }


   and pass map to main recyclerview adapter along with commonDateList;

Upvotes: 1

Harikrishnan
Harikrishnan

Reputation: 8065

You don't need to use any third party libraries. You can make use of ExpandableListView without actually making it "expand and collapse" to do the exact same thing which you need. See my answer for this post. The advantage here is that you can deal with this as easily as you deal with an ExpandableListView, with no custom code. You only need to add one line to what is otherwise a standard ExpandableListView adapter.

@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
    ...
    ((ExpandableListView) parent).expandGroup(groupPosition);
    ...
}

Your section headers, you can set as a group view and the list items as children. Your data structure need to be updated before being passed on to the adapter. It need to be a grouped data, not a plain list which you have to pass to the expandable adapter(for example an array of classes, each instance that contain a String property for group header and an ArrayList of IncomeExpense objects). And when you update the data, make the update in the corresponding group, instead of the entire data.

Upvotes: 1

Related Questions