How to make recyclerview Expandable without using third party libraries - java

I want to create a list of items using RecyclerView and want to expand particular item when clicked (Like in phone call list ). I want to achieve this without using any library. Can anyone help ?

Get child data list as a Member of Parent data in dataset.
And, at click event of RecyclerView row, use them like this..
here
mdataSet is main dataset for RecyclerView
final TitleHolder holder = (TitleHolder) h;
final Model model = (Model) mdataSet.get(position);
holder.txt_title.setText(model.getTitle());
holder.childItem = model;
holder.txt_title.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (model.getChildList() == null) {
//collapse
((Model) mdataSet.get(mdataSet.indexOf(holder.childItem))).isExpanded = false;
holder.arrow.startAnimation(AnimationUtils.loadAnimation(context, R.anim.arrow_reverse));
model.childList = new ArrayList<ModelData>();
int count = 0;
int pos = mdataSet.indexOf(holder.childItem);
while (mdataSet.size() > pos + 1 && mdataSet.get(pos + 1).type == Model.VIEW_CHILD) {
model.childList.add((ModelData) mdataSet.remove(pos + 1));
count++;
}
notifyItemRangeRemoved(pos + 1, count);
} else {
//expand
((Model) mdataSet.get(mdataSet.indexOf(holder.childItem))).isExpanded = true;
holder.arrow.startAnimation(AnimationUtils.loadAnimation(context, R.anim.arrow));
int pos = mdataSet.indexOf(holder.childItem);
int index = pos + 1;
for (ModelData i : model.getChildList()) {
mdataSet.add(index, i);
index++;
}
notifyItemRangeInserted(pos + 1, index - pos - 1);
model.childList = null;
}
}
});
if (((Model) mdataSet.get(mdataSet.indexOf(holder.childItem))).isExpanded) {
holder.arrow.startAnimation(AnimationUtils.loadAnimation(context, R.anim.arrow));
}
Here, I will add child data to Main dataset at click event on txt_title
Again, use Title(parent) and data(child) as two different ViewTypes like this
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == VIEW_TITLE) {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_expand_title, parent, false);
return new TitleHolder(itemView);
} else {
View itemView = LayoutInflater.from(parent.getContext())
.inflate(R.layout.row_wallet_history, parent, false);
return new DataHolder(itemView);
}
}
OR
If your child view is fix (which you want to expand/collapse) then wrap them inside layout and, make that Layout visible/Gone with animation in order to achieve expand collapse effect
Refere this link to make them animated

Related

Recylerview add space below last item added in list ( ItemDecoration not updating)

I'm trying to have a space at the end of the last item in a RecyclerView.
For this I am using a ItemDecoration, but when adding a new item, both the previos item, and the new item will have the Bottom Offset. If I add a 3rd Item, the first item will no longer have offset, but the 2nd and newly added 3rd will have.
Below is my ItemDecoration class
class BottomOffsetDecoration : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
//dimension of offset
val bottomOffset = view.context.resources.getDimension(R.dimen.player_list_offset).toInt()
val dataSize = state.itemCount
val position = parent.getChildAdapterPosition(view)
//Logic of showing space or not
if (dataSize > 0 && position == dataSize - 1) {
outRect.set(0, 0, 0, bottomOffset)
} else {
outRect.setEmpty()
}
}
}
This gets added to my Recyclerview with Databinding like this:
#BindingAdapter(value = ["adapterListener", "itemList"])
fun RecyclerView.bindList(adapterListener: PlayerListItemListener, itemList: List<PlayerListItem>) {
var adapter = this.adapter
//copy the list as DiffUtils will ignore it if it is the same list every call
val listCopy = ArrayList(itemList)
if (adapter == null) {
adapter = PlayerListAdapter(adapterListener)
this.apply {
setAdapter(adapter)
//DECORATION
addItemDecoration(BottomOffsetDecoration())
setHasFixedSize(true)
}
}
(adapter as PlayerListAdapter).submitList(listCopy)
}
As for Recylerview Adapter I am using the ListAdapter with DiffUtils, not the RecyclerView.Adapter
Question is: how do I remove the offset from the last item once a new Item is added?
DISCLAIMER: I can not use the below for my case:
android:paddingBottom="50dp"
android:clipToPadding="false"
Thank you in advance!
So, for anyone else coming across this problem, here is how I solved it.
You need to call invalidateItemDecorations() after ListAdapter applied the changes to the list.
In my case I used submitList(list, commitCallback), like below inside my BindingAdapter method:
(adapter as PlayerListAdapter).submitList(listCopy) {
post {
//remove decoration from other elements so the bottom offset is added to only the last element inserted
invalidateItemDecorations()
}
}
I also updated my getItemOffset(), this was not part of the problem, but I like this code more:
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
super.getItemOffsets(outRect, view, parent, state)
val bottomOffset = view.context.resources.getDimension(R.dimen.player_list_offset).toInt()
val adapter = parent.adapter
if (adapter == null || adapter.itemCount == 0) {
return
}
if (parent.getChildAdapterPosition(view) == adapter.itemCount - 1) {
outRect.bottom = bottomOffset
} else {
outRect.bottom = 0
}
}
Happy coding!

How to change background colour to specific viewholder items in a RecycleView?

I am trying to change background color in specific item(s) in a RecycleView.
Because I need to set text too, I have the following code that works fine:
protected void populateViewHolder(RankingViewHolder viewHolder, final Ranking model, int position)
{
final Context mContext = getActivity().getApplicationContext();
viewHolder.txt_name.setText(model.getUserName());
viewHolder.txt_score.setText(String.valueOf(model.getScore()));
viewHolder.txt_class.setText(model.getUser_class());
Picasso.with(mContext).load(model.getAvatarUrl()).error(R.drawable.ic_people_black_24dp).into(viewHolder.personPhoto);
int totalRanking = adapter.getItemCount();
int realRank = totalRanking - viewHolder.getAdapterPosition();
viewHolder.ranknumber.setText("# "+String.valueOf(realRank));
}
This works as I want and realRanktakes the correct values, and the viewHolder.ranknumber.setText("# "+String.valueOf(realRank));
Sets the right text with no problem.
Now I am trying (as I got a number/text result correct, to make an if statement like this:
if(adapter.getItemCount() -viewHolder.getAdapterPosition() == 0)
{
viewHolder.itemView.setBackgroundColor(Color.GREEN);
}
if(adapter.getItemCount() -viewHolder.getAdapterPosition() == 1)
{
viewHolder.itemView.setBackgroundColor(Color.YELLOW);
}
if(adapter.getItemCount() -viewHolder.getAdapterPosition() == 2)
{
viewHolder.itemView.setBackgroundColor(Color.BLUE);
}
(I tried with String.valueOf(realRank)equality, with realRankequality too)
In all cases I have the same result. The color changes as its should at positions 1,2,3 BUT it changes at positions 7,8,9 and 14,15,16 and 21,22,23 etc.
What am I missing here?
public class RankingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener
{
private ItemClickListener itemClickListener;
public TextView txt_name, txt_score, txt_class, ranknumber;
public ImageView personPhoto;
public RankingViewHolder(View itemView)
{
super(itemView);
txt_name = itemView.findViewById(R.id.txt_name);
txt_score = itemView.findViewById(R.id.txt_score);
personPhoto = itemView.findViewById(R.id.person_photo);
txt_class = itemView.findViewById(R.id.txt_class);
ranknumber = itemView.findViewById(R.id.ranknumber);
itemView.setOnClickListener(this);
}
public void setItemClickListener(ItemClickListener itemClickListener) {
this.itemClickListener = itemClickListener;
}
#Override
public void onClick(View view) {
itemClickListener.onClick(view , getAdapterPosition(),false);
}
}
The adapter:
adapter = new FirebaseRecyclerAdapter<Ranking, RankingViewHolder>(
Ranking.class,
R.layout.layout_ranking,
RankingViewHolder.class,
rankingTbl.orderByChild("score").limitToFirst(100)
)
This line of code int realRank = totalRanking - viewHolder.getAdapterPosition();gives a number (1,2,3,4,5,6 etc.) Why i cannot use this number to check equality?
Notice
Keeping this code for NOT working solution, just for future reference:
if(position == 0){
viewHolder.itemView.setBackgroundColor(Color.GREEN);
}
else if(position == 1){
viewHolder.itemView.setBackgroundColor(Color.YELLOW);
}
else if(position == 2){
viewHolder.itemView.setBackgroundColor(Color.BLUE);
}
else{
viewHolder.itemView.setBackgroundColor(Color.WHITE);
}
This changes the color BUT not for only 3 first items. As you scroll down, changes the color for every 3 first viewable items like before, meaning 1,2,3, 7,8,9, etc.
Update:
I dont use a custom adapter, i use FirebaseRecyclerAdapter.
Thanks to #Muhammad Haroon comment i checked that has getItemViewType. So now i m trying with it like
position =adapter.getItemViewType( 0);
if(position == 0){
viewHolder.itemView.setBackgroundColor(Color.GREEN);
}
Not working for now, but i think its the correct direction...
Update 2
With position its not possible as RecycleView recycles the views so i have the same result. The working code is
if (linearLayoutManager.findFirstVisibleItemPosition() > 0) {
viewHolder.itemView.setBackgroundResource(R.drawable.blackframe);
}
else{
viewHolder.itemView.setBackgroundResource(R.drawable.goldframe);
}
Works fine except that after scrolling loosing the change of background.
So as we want and need the perfection, any idea for keeping even after scroll?
hi try add this in your Adapater it may solve your problem.
#Override
public int getItemViewType(int position) {
return position;
}
Please give this a try
override in your custom adapter
#Override
public long getItemId(int position) {
return position;
}
and in in your adapter object:
myAdapter.setHasStableIds(true);
In populateViewHolder add these line of code
if(position == 0){
viewHolder.itemView.setBackgroundColor(Color.GREEN);
}
else if(position == 1){
viewHolder.itemView.setBackgroundColor(Color.YELLOW);
}
else if(position == 2){
viewHolder.itemView.setBackgroundColor(Color.BLUE);
}
else{
viewHolder.itemView.setBackgroundColor(Color.WHITE);
}
position is a parameter in populateViewHolder.

add view in linearlayout programmatically working slowly

My goal is to add my custom view inside LinearLayout.I have custom arrayList and I would to add custom views with for loop.Here is a my code snippet
public void replaceCustomView() {
for (int i = 0; i < insertDataItems().size(); i++) {
final LayoutInflater inflater = LayoutInflater.from(this);
final View reView = inflater.inflate(R.layout.item_parent_child_listing, null, false);
final TextView parentName = reView.findViewById(R.id.tv_parentName);
final ImageView headerImageView = reView.findViewById(R.id.header_imageView);
final LinearLayout linearLayout_childItems = reView.findViewById(R.id.ll_child_items);
final RelativeLayout headerLayout = reView.findViewById(R.id.header_layout);
final RelativeLayout headerImageLayout = reView.findViewById(R.id.header_image_layout);
parentName.setText(insertDataItems().get(i).getParentName());
if (insertDataItems().get(i).getChildDataItems() != null) {
headerImageLayout.setVisibility(View.VISIBLE);
headerImageLayout.setVisibility(View.VISIBLE);
for (int j = 0; j < insertDataItems().get(i).getChildDataItems().size(); j++) {
final LayoutInflater childInflater = LayoutInflater.from(this);
final View childView = childInflater.inflate(R.layout.z_history_child_item, null, false);
final TextView key = childView.findViewById(R.id.u_key);
final TextView value = childView.findViewById(R.id.u_value);
key.setText(insertDataItems().get(i).getChildDataItems().get(j).getKey());
value.setText(insertDataItems().get(i).getChildDataItems().get(j).getValue());
linearLayout_childItems.addView(childView);
}
} else {
headerImageLayout.setVisibility(View.GONE);
headerLayout.setBackgroundColor(Color.parseColor("#e8e8e8"));
}
linearLayout_childItems.setVisibility(View.GONE);
if (insertDataItems().get(i).getParentName().length() > 0) {
if (insertDataItems().get(i).isAllowDisable()) {
headerImageView.setVisibility(View.GONE);
linearLayout_childItems.setVisibility(View.VISIBLE);
} else {
headerImageView.setVisibility(View.VISIBLE);
linearLayout_childItems.setVisibility(View.GONE);
}
} else {
linearLayout_childItems.setVisibility(View.VISIBLE);
headerLayout.setVisibility(View.GONE);
}
replaceLayout.post(() -> replaceLayout.addView(reView));
}
}
I call this function like this
runOnUiThread(() -> replaceCustomView());
Custom views adding successfully,but my problem is that in a first time activity is slowly.Android need to much time to add view.My custom array's size is 20.Is it a any way to add views step by step ,not add all views each time?
What's a best practice ?
thanks
Please do not use the concept of adding runtime components when the items are more. This will cause the activity to share the memory with view rendering logic and you'll be able to recycle the views. this kind of implementation can be used only when there are a few (may be 2 to 5) items and can't be achieved with recyclerview due to some UI limitations.
Hence use a RecyclerView to load the items and you can use the concept of adding custom items inside a child item to achieve this functionality.

notifyDataSetChanged() not working with custom Adapter View

I have seen this question answered a few times, however none of the fixes have worked for me, so i'm reaching out.
I have built an app that features the Diolor Swipeable Cards Library (here) and now am trying to implement Course Card Filters.
Essentially when a user clicks a course filter we want to change the data that is being fed to the adapter.
Currently I am trying to update the data and calling notifyDataSetChanged() on the adapter, expecting the cards to refresh to show the new data set, however am finding that it is not refreshing at all.
Any help with this would be hugely appreciated.
All code below is from my Main Activity.
I declare the data set that i will be feeding to the adapter at the top of the activity:
ArrayList<CourseCardModel> courseCardModelList;
then in my onCreate() method I instantiate the adapter, attach it to the view, and call a generateCourseCards() method which populates the courseCardModelList with objects pulled from a firebase database.
// Set up and assign card adapter
ca = new CustomCardAdapter(CardsActivity.this, android.R.layout.simple_list_item_1, generateCourseCards());
flingContainer.init(CardsActivity.this, ca);
generateCourseCards() method
private ArrayList<CourseCardModel> generateCourseCards() {
Toast.makeText(getApplicationContext(), "Retrieving Courses", Toast.LENGTH_LONG).show();
courseCardModelList = new ArrayList<CourseCardModel>();
dbref = FirebaseDatabase.getInstance().getReference().child("courses");
// Retrieve the course data from Firebase db and cast as Course object
dbref.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
Log.e("Count " ,"" + snapshot.getChildrenCount());
for (DataSnapshot postSnapshot: snapshot.getChildren()) {
c = postSnapshot.getValue(Course.class);
System.out.println(c.getCourseName());
CourseCardModel model = new CourseCardModel();
model.setCourse(c);
courseCardModelList.add(model);
}
Collections.shuffle(courseCardModelList);
ca.notifyDataSetChanged();
}
#Override
public void onCancelled(DatabaseError databaseError) {
Log.e("The read failed: ", databaseError.getMessage());
}
});
return courseCardModelList;
}
Attempt to update the dataset (a simple shuffle for the time being) and refresh the cards
// Shuffle the collection and refresh the cards
Collections.shuffle(courseCardModelList);
ca.notifyDataSetChanged();
EDIT: added adapter code
public class CustomCardAdapter extends ArrayAdapter {
private TextView courseName, uniName, entryStandards, courseDuration, studyMode, qualification,
studentSatisfaction, gradProspects, t1, t2, t3, t4, t5, t6;
ArrayList<CourseCardModel> items;
View v;
LayoutInflater vi;
public CustomCardAdapter(Activity context, int resource, ArrayList<CourseCardModel> courses) {
super(context, resource, courses);
vi = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
#NonNull
#Override
public View getView(int position, View convertView, ViewGroup parent) {
v = convertView;
if (v == null) {
v = vi.inflate(R.layout.course_card_inner_template, parent , false);
}
CourseCardModel c = (CourseCardModel) getItem(position);
if (c != null) {
courseName = (TextView) v.findViewById(R.id.courseCardCourseName);
uniName = (TextView) v.findViewById(R.id.courseCardUniName);
entryStandards = (TextView) v.findViewById(R.id.courseCardEntryStandards);
courseDuration = (TextView) v.findViewById(R.id.courseCardCourseDuration);
studyMode = (TextView) v.findViewById(R.id.courseCardStudyMode);
qualification = (TextView) v.findViewById(R.id.courseCardQualification);
studentSatisfaction = (TextView) v.findViewById(R.id.courseCardStudentSatisfaction);
gradProspects = (TextView) v.findViewById(R.id.courseCardGraduateProspects);
t1 = (TextView) v.findViewById(R.id.cardTV1);
t2 = (TextView) v.findViewById(R.id.cardTV2);
t3 = (TextView) v.findViewById(R.id.cardTV3);
t4 = (TextView) v.findViewById(R.id.cardTV4);
t5 = (TextView) v.findViewById(R.id.cardTV5);
t6 = (TextView) v.findViewById(R.id.cardTV6);
v.setBackgroundResource(R.drawable.newcard);
courseName.setText(c.getCourse().getCourseName());
uniName.setText(c.getCourse().getUniversity());
entryStandards.setText(c.getCourse().getEntryStandards());
courseDuration.setText(c.getCourse().getCourseDuration());
studyMode.setText(c.getCourse().getStudyMode());
qualification.setText(c.getCourse().getQualification());
studentSatisfaction.setText(c.getCourse().getStudentSatisfaction().toString() + " / 5");
gradProspects.setText(c.getCourse().getGradProspects() + " / 100");
}
if(position ==0)
{
//float alpha = (float) 0.8;
//v.setAlpha(alpha);
courseName.setVisibility(View.VISIBLE);
}
else if (position == 1){
// Prepare the View for the animation
v.setVisibility(View.VISIBLE);
float alpha = (float) 0.8;
float alpha2 = (float) 0.3;
courseName.setAlpha(alpha2);
uniName.setAlpha(alpha2);
entryStandards.setAlpha(alpha2);
courseDuration.setAlpha(alpha2);
studyMode.setAlpha(alpha2);
qualification.setAlpha(alpha2);
studentSatisfaction.setAlpha(alpha2);
gradProspects.setAlpha(alpha2);
t1.setAlpha(alpha2);
t2.setAlpha(alpha2);
t3.setAlpha(alpha2);
t4.setAlpha(alpha2);
t5.setAlpha(alpha2);
t6.setAlpha(alpha2);
v.setAlpha(alpha);
}
else {
v.setVisibility(View.INVISIBLE);
}
return v ;
}
public void updateData(ArrayList<CourseCardModel> courseCardModels) {
this.items = courseCardModels;
notifyDataSetChanged();
}
}
Problem is in this method.
public void updateData(ArrayList<CourseCardModel> courseCardModels) {
this.items = courseCardModels;
notifyDataSetChanged();
}
here you are giving another array reference to your adapter.
Just rewrite as below.
public void updateData(ArrayList<CourseCardModel> courseCardModels) {
this.items.clear();
this.items.addAll(courseCardModels);
notifyDataSetChanged();
}
Without adapter class provided my first guess would be that you messed the references up. Maybe you are shuffling the data that is not referenced from the adapter. Once you share your adapter's code, I'll update my answer.
== EDIT ==
Avoid referencing some external collection of data from adapter, and updating that referenced data. Updating adapter/list data should be done using adapter's interface and methods such as add(), addAll() or remove() It might happen that (parent) adapter makes clone/copy of your data and in that case updating external/referenced collection is not doing any good.
You're extending an ArrayAdapter which holds his own array of models (the array passed to the constructor). If you would like to update the items, do something like this:
ca.clear();
for (CourseCardModel object : courseCardModelList) {
ca.insert(object, ca.getCount());
}
ca.notifyDataSetChanged();
Or you can override the getItem method and return an item from your items array.
And another option would be extending BaseAdapter instead of the ArrayAdapter.

Android - GridView setSelection not working

Currently using a tabbar/viewpager with fragments setup for this project. Fragment 2 contains a gridview. At app startup I'm trying to select a gridview cell by default - but no matter what I do it does not 'select'. I'm beginning to wonder if this is because at the time the selection tries to take place, the gridview is off screen (page/fragment 2 of the viewpager).
What I'm doing is after the getView method of the GridViewAdapter is initially complete (I'm comparing position to total number of possible cells to determine this) I fire a listener message to select the default cell in the GridView. I did it this way to (a) ensure that the cell I'm trying to select is non-null, and (b) I wondered if the getView method was resetting the selection somehow.
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
// * Other code that sets up the view
if (listener!=null) {
if ((list.size()-1)==position) {
Log.d(TAG, "Today position set: " + todayPosition);
listener.todayPositionFound(todayPosition);
}
} else {
Log.d(TAG, "LISTENER IS NULL");
}
return row;
}
and then...
public void todayPositionFound(final int position) {
// * ------------------------
// * On startup, select today
// * ------------------------
mCurrentlySelectedDate = DateHelper.todayAsString();
Log.d(TAG, "Todays Position Found: " + position);
View v = calendarView.getChildAt(position);
if (v!=null) {
Log.d(TAG, "V not NULL - SELECTING");
v.setSelected(true);
}
Log.d(TAG, "SELECTED? " + calendarView.getSelectedItemPosition());
}
All of this goes off without a problem, aside from the fact that the view is then NOT selected. Furthermore, when I getSelectedItemPosition it returns -1 ... even though I just 'selected' position 16. Any thoughts on this would be much appreciated. Thank you!
To get this working I used a Handler and Runnable:
public void todayPositionFound(final int position) {
Handler h = new Handler();
Runnable r =new Runnable() {
public void run() {
View v = calendarView.getChildAt(position);
if (v!=null) {
v.setSelected(true);
}
}
};
h.postDelayed(r, 500);
}
If someone has a better solution please do let me know. Thanks!
You can only update the list scroll position after the the List/GridView has been drawn. This happens a short time after onCreate() or onResume() or onCreateView() has been called.
You could try using a Global layout listener to tell you when the GridView has been drawn, for example:
GridView calendarView = (GridView)findViewById(R.id.YOUR_VIEW_ID);
ViewTreeObserver viewTreeObserver = calendarView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
this.calendarView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
View v = calendarView.getChildAt(position);
if (v!=null) {
v.setSelected(true);
}
}
});

Categories