如何在 RecyclerView 中设置粘性 headers? (有或没有外部库)

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

我正在使用房间数据库,我有一个 table 从那里我得到了一个 LiveData 列表。 在 table 中有一列日期,我在其中存储当前日期。默认选择当前日期,但用户也可以在向数据库中插入数据时更改日期。

我想以这种方式在 recyclerview 中显示此数据 https://imgur.com/RYegFpG

我想根据 header 月份和年份以及它下面那个月份年份的所有条目来划分此数据。

例如,用户在2019年10月插入数据,我希望这个"October 2019"作为recyclerview中的header,以及它下面这个月的所有条目。就像这样,所有月份的条目都应该以相同的方式显示,因为每个下个月都变成 header 并且那个月的条目在它下面。

我试图通过

来实现这一点
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);
            }
        }

但是此代码中的问题是,当用户从设置中更改月份并将一些条目放入数据库时​​,那一年的月份条目已经存在于 recyclerview 中。它在 recyclerview 中创建该现有月份的另一个 header。但我希望将这些条目放入现有月份 header,而不是创建该月份的新 header。

在不使用外部库的情况下实现此目标的最佳方法是什么,因为在这种情况下我不想依赖外部库。

我是编程新手。

已更新

在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);
        }
    });

在回收适配器中

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();
}

您不需要使用任何第三方库。您可以使用 ExpandableListView 而无需实际使用 "expand and collapse" 来做您需要的完全相同的事情。 See my answer for this post。这里的优点是您可以像处理 ExpandableListView 一样轻松地处理它,而无需自定义代码。您只需向标准 ExpandableListView 适配器添加一行。

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

您的部分 headers,您可以将其设置为群组视图并将列表项设置为 children。您的数据结构需要在传递给适配器之前进行更新。它需要是分组数据,而不是必须传递给可扩展适配器的普通列表(例如 类 的数组,每个实例包含一个 String 属性 组header 和 IncomeExpense objects 的 ArrayList)。并且当你更新数据时,在相应的组中进行更新,而不是整个数据。

父回收者视图

<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>

它的适配器

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);
        }
    }
}

Recyclerview 内的 Reyclerview

<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>

它的适配器

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);
        }
    }
}

内部回收视图行

<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 或将数据发送到适配器的片段

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;
  1. 首先在你的 setIncomeExpenseList 函数中清除你之前的列表。
  2. 然后重新分配包含来自数据库的完整数据的新列表。
  3. 将 notifyDataSetChanged() 更改为 notify() 或 notifyAll()。

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