Problem: Glitch bug with RecyclerView's child's item ImageView
I have a RecyclerView.Each item has ImageView with id "like" which is basically an empty star.
When we click on some item's "Like", for example, the first, the star is changing from empty filled in yellow, which means that item is liked.
When we click on the first item everything is OK, everything is as it should be, but at the same time, we have a bug, changing the sixth item's "like" to a filled star, which shouldn't be done, as this item wasn't liked yet.
If we click on second item's like - the seventh item would have the same bug.
To fill the item's in ViewHolder I have a model - Recipe.
open class Recipe constructor(open var label: String,
open var image: String,
open var url: String,
open var ingredients: RealmList<Ingredient>?,
open var calories: Float,
open var totalWeight: Float,
open var id: Int,
open var isFavorite: Boolean) : RealmObject()
So when clicking on some item I check whether it isFavorite or not, and change the "like" ImageView accordingly.
Have anyone got any ideas how I should fix this?
I tried to turn off recycling of items in the adapter, but unfortunately, in this case, the "liked" state isn't saved too.
OnClick listener for "like" button
itemView.like.onClick {
if (recipe.isFavorite) {
itemView.like.image = ContextCompat.getDrawable(itemView.context, R.drawable.ic_star_before_like)
recipe.isFavorite = false
DatabaseService.removeRecipeFromFavorites(recipe)
itemView.context.toast("Recipe removed from favorites.")
} else {
itemView.like.image = ContextCompat.getDrawable(itemView.context, R.drawable.ic_star_after_like)
recipe.isFavorite = true
DatabaseService.addNewFavoriteRecipe(recipe)
itemView.context.toast("Recipe added to favorites.")
}
}
XML-File
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="0.8"
android:gravity="center|right"
android:orientation="horizontal">
<ImageView
android:id="#+id/like"
android:layout_width="#dimen/icon_size_card"
android:layout_height="#dimen/icon_size_card"
android:padding="5dp"
android:layout_marginRight="#dimen/icon_margin_card"
android:src="#drawable/ic_star_before_like" />
<ImageView
android:id="#+id/share"
android:layout_width="#dimen/icon_size_card"
android:layout_height="#dimen/icon_size_card"
android:padding="5dp"
android:layout_marginRight="#dimen/icon_margin_card"
android:src="#drawable/ic_share" />
<ImageView
android:id="#+id/save"
android:layout_width="#dimen/icon_size_card"
android:layout_height="#dimen/icon_size_card"
android:padding="5dp"
android:layout_marginRight="#dimen/icon_margin_card"
android:src="#drawable/ic_save_recipe" />
<ImageView
android:id="#+id/expand"
android:layout_width="#dimen/icon_size_card"
android:layout_height="#dimen/icon_size_card"
android:padding="5dp"
android:layout_marginRight="#dimen/icon_margin_card"
android:src="#drawable/ic_down_arrow" />
</LinearLayout>
RecyclerView reuses the viewholders it creates with onCreateViewHolder. To put it simply, think of it as the views going off screen on the top coming back around on the bottom, and vica versa. Therefore, the nice way of solving what you want is the following:
Set the icon to the right drawable in the onBindViewHolder method. This way, whether a viewholder is being bound for the first time, or had a drawable reflecting a recipe's favourited state already, it will be refreshed properly.
Without knowing what your adapter looks like, this should give you an idea of how to do this (the example code assumes you have your recipes in some list called recipes):
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val recipe = recipes[position]
val itemView = holder.itemView
itemView.like.image = ContextCompat.getDrawable(itemView.context,
if(recipe.isFavorite) R.drawable.ic_star_before_like else R.drawable.ic_star_after_like)
}
In the listener, modify the Recipe the same way you're doing it now, but instead of then setting the image directly, notify the adapter that the recipe at the given position has changed (notifyItemChanged(position)), which will make it re-bind that item, refreshing the icon to reflect the new state. Again, I don't know where you're setting up the listener, but I assume you know what position the recipe is at in the list there.
RecyclerView is built to cache views and reuse them. When you scroll, your views are recycled and loaded from the memory.
After you have made changes to your underlying data model attached to the adapter, it is important to notify the adapter about the same. In your onClick method, after you've made the changes, make sure you call adapter.notifyDataSetChanged() or just notifyDataSetChanged() if you're calling it directly from your adapter class.
Related
I have a separate layout that I want to call onClick and update a field on callback
<include
android:onClick="#{() -> viewModel.changeItem(2)}"
layout="#layout/item"
app:attr="#{viewModel.title}"
app:desc="#{viewModel.description}"
app:active="#{viewModel.isSelected}"
/>
But it returns the following binding error:
Cannot find the setter for attribute 'android:onClick' with parameter
type lambda on com.X.databinding.ItemBinding.
But I can binding on other views
<TextView
android:onClick="#{() -> viewModel.changeItem(1)}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
What I suggest you is to call the onClick directly from code.
Setup an ID for your view in your .xml, then set something like this :
val item = findViewById(R.id.your_id) etc.
item.onClick { functionYouWantToCall() }
However, I do not know if this is possible to set an ID or an onClickListener on an include layout.
If you can not do it, simply put your include inside a LinearLayout, then set your onClick on it.
I used to set onClicks in the .xml too, but I think it is much more efficient to set it in the code.
Moreover, I do not know if you can set arguments in a function you call from the xml.
I am using a DrawerLayout on my Android app which works fine but I am having problems with the menu itself, where I want to have a collapsible section and some static items in the menu.
The exact layout I want to achieve is like this:
Drawer Title
-> Expandable List
Menu Item 1
Menu Item 2
And when the Expandable list is open to have something like this:
Drawer Title
-> Expandable List
Expandable List Item 1
Expandable List Item 2
Menu Item 1
Menu Item 2
The problem I have is that the menu doesn't seem to sit in the flow of the view like other elements do, so if I have my ExpandableListView inside the NavigationView that contains the menu, it opens over the other menu items and there doesn't seem to be a way to move them.
To try and address this I have moved the ExpandableListView into a separate layout that provides the header for the menu. This now draws everything correctly but I can't open the list view any more, which is definitely a problem. In fact it does open - or at least the onGroupExpandListener is triggered - but although the height of the header is set to wrap_content and I invalidate it on expand and collapse, the size of the header remains resolutely the same. To work around this I am manually changing it's size in the onGroupExpandListener and onGroupCollapseListener as follows.
In my layout I have a NavigationView like this:
<android.support.design.widget.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="#menu/drawer_view" >
</android.support.design.widget.NavigationView>
The header looks like this:
<LinearLayout
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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="70dp"
android:background="#color/colorPrimary" >
<ImageView
android:layout_width="70dp"
android:layout_height="70dp"
android:padding="10dp"
app:srcCompat="#drawable/main_icon" />
<TextView
android:id="#+id/drawer_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:layout_gravity="left|start|center"
android:layout_marginStart="70dp"
android:paddingStart="5pt"
android:text="Options"
android:textColor="#FFFFFF"
android:textSize="16pt"
android:textStyle="bold" />
</RelativeLayout>
<ExpandableListView
android:id="#+id/menu_expandable_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom" >
</ExpandableListView>
</LinearLayout>
In my code, I am currently setting up the ExpandableListView ( complete with a bunch of superfluous hacks because I'm trying to figure out the problem ) like this:
private void setOptionsMenuItems(final NavigationView menuContainer) {
final View v = menuContainer.inflateHeaderView(R.layout.drawer_header);
final ExpandableListView optionsMenu = (ExpandableListView) v.findViewById(R.id.menu_expandable_list);
final MyExpandableListAdapter adapter = new MyExpandableListAdapter(this, allTheItems );
optionsMenu.setAdapter(adapter);
optionsMenu.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
#Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
setCurrentItem(allTheItems[childPosition]);
return true;
}
});
optionsMenu.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
#Override
public void onGroupExpand(int groupPosition) {
v.setMinimumHeight(v.getChildAt(0).getHeight() + adapter.getOpenHeight());
v.invalidate();
}
});
optionsMenu.setOnGroupCollapseListener(new ExpandableListView.OnGroupCollapseListener() {
#Override
public void onGroupCollapse(int groupPosition) {
v.setMinimumHeight(v.getChildAt(0).getHeight() + adapter.getClosedHeight());
v.invalidate();
}
});
}
What this version of the code does is to enlarge and shrink the header correctly when the group is opened or closed ( adapter.getOpenHeight()/getClosedHeight() gives you the dimension you might expect ) but the actual child items never get rendered. Doing the same enlargement in an onGroupClickListener is no different, although I suspect that the child elements are being culled from the view because the layout system assumes they are invisible, rather than the view being resized. If I manually make the header taller, so that everything is in view when the ExpandableList is expanded, it opens and closes as normal but obviously this does not allow me to move the other menu items up and down, which is the whole point.
This all seems inordinately complex for what must be a common use case so if I am taking completely the wrong approach here please let me know. All I want is for the first item in my menu to be an ExpandableList and for the other menu items to be regular menu items that move down when the the ExpandableList is opened and move back up when it is closed.
What do I need to do to have a working collapsible list of options in my menu?
Edit: With some more time on this, what I think is happening is that the list is bypassing drawing the new sections because it has measured that there is no visible room for them - although the resize can be triggered from the onGroupClickListener the actual change in visible size doesn't occur until the next drawing pass, so when the base ListView tests to see whether there is room to write the list, it is coming up false.
I did not find a way to do this using a conventional menu- instead the solution I came to was adding my other menu items as ExpandableListView groups so they simply appear at the top level of the list view. This allowed me to create the effect I was looking for, at the cost of not using menus the 'correct' way.
I have a vertical LinearLayout in my app where every row should get a more complex sub-view consisting of an image and two lines of text. Now I want to make the complete row clickable in order to start an other action. This means the whole thing should react on a click: the base view which holds the other elements, the image and even the two lines of text.
My question: what is the best approach for this? I can't use a simple button for the complete row and as far as I know I can't set an onClick-Handler for view/image/labels?
Any idea how this could be done?
Thanks!
The easiest option would be to put android:descendantFocusability="blocksDescendants" on your parent view, this will block any child view from taking clicks from the parent, then put a click listener on the parent only.
This is assuming you don't care on which child view exactly the user clicked within the parent.
<LinearLayout
android:id="#+id/parent"
...
android:clickable="true"
android:descendantFocusability="blocksDescendants" >
<TextView
... />
<ImageButton
... />
</LinearLayout>
and in code:
LinearLayout parent = (LinearLayout) findViewById(R.id.parent);
parent.setOnClickListener(...);
You just need to add clickable tag true in xml to your main view eg relativelayout or linearlayout.
That will work
This is how I did it. Just loop over the childen and make then non clickable. This works even if you are using clickable items inside root view.
rootView.apply {
descendantFocusability = ViewGroup.FOCUS_BLOCK_DESCENDANTS
isClickable = true
setOnClickListener {
Snackbar.make(it, "$data", Snackbar.LENGTH_LONG).show()
}
children.forEach {
it.isClickable = false
}
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:EMVideoView="http://schemas.android.com/apk/res-auto"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:background="#color/MUVCastleRock">
<com.devbrackets.android.exomedia.ui.widget.EMVideoView
android:id="#+id/video_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
EMVideoView:useDefaultControls="true" />
How do I change the color of the default controls (where you would see the play button)? I don't want to change the background behind the video, but instead the play bar. This is because my background is grey and so I want to change the color so that the bar is more visible.
EDIT:
#Shank suggested that I replace the images of the buttons in /exomedia/ui/widget/VideoControls.java
Although, I wasn't trying to change the images of these buttons, analyzing this class lead to my answer.
There are several functions within this class to change the settings of the default video controls.
You can set a title, description, subtitle, change out the button images (as Shank suggested), and solve my particular problem by changing the characteristics of the containers that contain the video controls (named controlsContainer).
The default container is initialized in retrieveViews() by the line:
controlsContainer = (ViewGroup) findViewById(R.id.exomedia_controls_interactive_container);
Using this, I simply called this reference from my EMVideoView and changed the color as appropriate:
emVideoView = (EMVideoView) myView.findViewById(R.id.video_view);
ViewGroup textContainer = (ViewGroup) emVideoView.findViewById(R.id.exomedia_controls_interactive_container);
textContainer.setBackgroundColor(ContextCompat.getColor(getContext(),R.color.myColor));
Other useful methods that I discovered were as such:
emVideoView.getVideoControls().setTitle("title");
emVideoView.getVideoControls().setDescription("description");
emVideoView.getVideoControls().setSubTitle("sub");
emVideoView.getVideoControls().setPlayPauseImages(R.drawable.logo,R.mipmap.ic_launcher);
I am making an android app where the same imageview is displayed every time the user enters a value. but I want the imageview to be changed every time the user enters a value. I wrote a code to do this but the problem is after the first image is displayed I face black screen and it takes me to the first page in the app, it doesn't crash just shows black screen.
here is the imageview code in xml file:
<ImageView
android:id="#+id/ImageSuccess"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="center"
android:layout_marginBottom="20dip"
android:layout_marginTop="20dip"
android:scaleType="centerInside"
android:src="#drawable/image1" />
and here where I changed the imageview in java file:
ImageView myImage= (ImageView) findViewById(R.id.ImageSuccess);
if(userVlue.equals("SCHOOL")){
myImage.setImageResource(R.drawable.image1);
}
else if(userVlue.equals("CAR")){
myImage.setImageResource(R.drawable.image2);
}
else if(userVlue.equals("TOY")){
myImage.setImageResource(R.drawable.image3);
}
One of myImage or userVlue is not set properly and probably is null.
Debug and share logs. Your partcular activity is getting distroyed probably because of nullpointexception hence returning to main activity in backtrack.
I tried with a sample app and your logic is working perfectly.
You are handling user action on some UI element, when you do findViewById, cotext is that UI element, so this method searches ImageSuccess in children of it. Try giving page layout as rootlayout and do rootlayout.findViewById("ImageSuccess");
I found the solution finally, the black screen appeared because the images were only in the drawable file, so I needed to add the images in all drawbridge folders such as drawable-ldp, drawable-ldpi etc...., so that the app can work in different phone sizes.