I'm building a UI that consists of multiple CardViews inside a RecyclerView.
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView
android:id="#+id/card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
app:cardCornerRadius="5dp"
app:cardElevation="1dp"
app:cardBackgroundColor="#ddffca"
android:animateLayoutChanges="true"
xmlns:android="http://schemas.android.com/apk/res/android"
>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:padding="10dp"
>
<TextView
android:id="#+id/tvHello"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="Hello there!"
/>
<TextView
android:id="#+id/test"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:visibility="gone"
android:gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/tvHello"
android:text="GENERAL KENOBI!"
/>
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
Inside I have specified that on click even I will have my test text animated to appear. It's working great and I'm mostly happy with the results. But as soon as I add a few cards inside the RecyclerView the animation starts working a bit strange. What I mean is views that are not touched are not animating properly considering the other views have changed their size. Instead I see another view jumping to its new position without any animation.
How can I make it animate according to other views?
EDIT
I have also provided my code from onBindViewHolder. It's in Kotlin though:
override fun onBindViewHolder(
holder: OperationsViewHolder,
position: Int
) {
var card: CardView = holder.cardView
card.setOnClickListener {
if (!operations.get(position).selected!!) {
ObjectAnimator.ofFloat(card, "translationZ", 1f, 10f)
.start()
holder.test.visibility = View.VISIBLE;
operations.get(position)
.selected = true
} else {
ObjectAnimator.ofFloat(card, "translationZ", 10f, 1f)
.start()
holder.test.visibility = View.GONE;
operations.get(position)
.selected = false
}
}
}
EDIT 2 I have also tried adding android:animateLayoutChanges="true" to all elements, didn't help
Not sure if this fits the constraints of your question but you don't necessarily have to animate the expanding/collapsing manually. The RecyclerView can provide that to you out-of-the-box by using notifyItemChanged() properly in your Adapter.
override fun onBindViewHolder(
holder: OperationsViewHolder,
position: Int
) {
var card: CardView = holder.cardView
if (operations.get(position).selected!!) {
holder.test.visibility = View.VISIBLE;
} else {
holder.test.visibility = View.GONE;
}
card.setOnClickListener {
if (!operations.get(position).selected!!) {
operations.get(position)
.selected = true
} else {
operations.get(position)
.selected = false
}
notifyItemChanged(position)
}
}
The above code removes the animation logic and instead calls notifyItemChanged every time the CardView is clicked. This tells the RecyclerView to re-render the item at that particular position and gives you an animation for the re-render for free.
Related
I am trying to make a reorderable GridLayout with drag and drop.
Basically, when I drag something in the GridLayout, it should reorder the GridLayout based on the dragging.
I have this GridLayout which has 6 FrameLayout inside.
<GridLayout
android:id="#+id/myGridLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3">
<FrameLayout
android:background="#android:color/holo_blue_dark"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1">
</FrameLayout>
<FrameLayout
android:background="#android:color/black"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1">
</FrameLayout>
<FrameLayout
android:background="#android:color/darker_gray"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1">
</FrameLayout>
<FrameLayout
android:background="#android:color/holo_green_dark"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1">
</FrameLayout>
<FrameLayout
android:background="#android:color/holo_purple"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1">
</FrameLayout>
<FrameLayout
android:background="#android:color/holo_red_dark"
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1">
</FrameLayout>
</GridLayout>
So It looks like the screenshot below
I have registered all the FrameLayouts in the GridLayout for LongClickListner and DragListener as per the codes below
for (i in 0 until myGridLayout.childCount) {
val childView = myGridLayout.getChildAt(i)
childView.setOnLongClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
it.visibility = View.INVISIBLE
it.startDragAndDrop(null, CustomDragShadowBuilder(it, lastTouch), it, 0)
} else it.startDrag(null, CustomDragShadowBuilder(it, lastTouch), it, 0)
true
}
childView.setOnDragListener { v, event ->
when (event.action) {
DragEvent.ACTION_DRAG_STARTED -> true
DragEvent.ACTION_DRAG_ENTERED -> {
val draggingView = event.localState as View
val dragEnteredIndex = myGridLayout.indexOfChild(v)
val draggingViewIndex = myGridLayout.indexOfChild(draggingView)
myGridLayout.removeViewAt(draggingViewIndex)
myGridLayout.addView(draggingView, dragEnteredIndex)
true
}
DragEvent.ACTION_DRAG_LOCATION -> true
DragEvent.ACTION_DRAG_EXITED -> true
DragEvent.ACTION_DROP -> true
DragEvent.ACTION_DRAG_ENDED -> {
val draggingView = event.localState as View
draggingView.post { run { draggingView.visibility = View.VISIBLE } }
true
}
else -> false
}
}
}
So, when you drag the red FrameLayout over to gray FrameLayout, the DragEvent.ACTION_DRAG_ENTERED is called. Then, I just remove the red FrameLayout and add it to the index of the gray FrameLayout where the DragShadow is over, so that I can reorder the GridLayout in real time. So, when I move around a FrameLayout in the GridLayout, I have got something like this, which is what I expect.
But, as you can see, when I release the drag, the red FrameLayout, or DragShadow goes back to its original position and console says I/ViewRootImpl[MainActivity]: Reporting drop result: false. Finally, the DragEvent.ACTION_DRAG_ENDED is called and make the red FrameLayout visible.
So, here are questions,
why the drag fails? Is this because I release the drag over the same FrameLayout, which is red one please?
Is there any way that I can prevent the DragShadow from getting back to its original position? What I want is just that when I release my drag, I just want the FrameLayout to appear at the new position, not getting back to its initial position.
Thank you guys in advance.
For those who have the same issues, I solved this isseus by using recyclerview with ItemTouchHelper which does have drag and drop featuers. You don't have to do your own codes.
I am working on an Android Kotlin project. I am applying animation on views. Starting from the basics, I am trying to animate an image view from the bottom of the screen to the center of the screen.
I have an XML layout with the following code.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorPrimaryDark"
tools:context=".MainActivity">
<LinearLayout
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/main_image_logo"
android:src="#drawable/memento_text_logo"
android:layout_width="#dimen/main_logo_image_width"
android:layout_height="wrap_content" />
<TextView
android:textColor="#android:color/white"
android:id="#+id/main_tv_slogan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/main_slogan"
/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
I am animating the logo image translating from the bottom to the center (where it is originally) in the activity with the following code.
private fun animateMainLogo() {
val valueAnimator = ValueAnimator.ofFloat(0f, main_image_logo.y)
valueAnimator.addUpdateListener {
val value = it.animatedValue as Float
main_image_logo.translationY = value
}
valueAnimator.interpolator = LinearInterpolator()
valueAnimator.duration = 1000
valueAnimator.start()
}
When I run the code, it is not animating the view. It is just there where it is and static. What is wrong with my code and how can I fix it?
translationY of view in layout is 0. If you want to animate it from bottom to current position - you should change translationY values from some positive value to 0.
private fun animateLogo() {
val translationYFrom = 400f
val translationYTo = 0f
val valueAnimator = ValueAnimator.ofFloat(translationYFrom, translationYTo).apply {
interpolator = LinearInterpolator()
duration = 1000
}
valueAnimator.addUpdateListener {
val value = it.animatedValue as Float
main_image_logo?.translationY = value
}
valueAnimator.start()
}
Same thing can be done this way:
private fun animateLogo() {
main_image_logo.translationY = 400f
main_image_logo.animate()
.translationY(0f)
.setInterpolator(LinearInterpolator())
.setStartDelay(1000)
.start()
}
Add this lines to LinearLayout and ConstraintLayout because without them LinearLayout will cut of parts of animated view when it is outside of LinearLayout bounds.
android:clipChildren="false"
android:clipToPadding="false"
Or make main_image_logo direct child of root ConstraintLayout. Here is result:
I have a ViewHolder that is meant to appear differently depending on whether it is on the left or right side of a two-column RecyclerView with a GridLayoutManager. Note the connector lines on either side of the view:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_marginTop="12px"
android:layout_marginBottom="12px"
android:layout_gravity="center"
android:gravity="center_vertical"
android:layout_height="wrap_content"
android:id="#+id/citation_select_holder">
<ImageView
android:src="#drawable/connector_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="#+id/citation_select_connector_right"/>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="348px"
android:layout_height="104px"
android:layout_weight="1"
android:background="#drawable/button_background_white"
android:id="#+id/citation_select_citation_holder">
<LinearLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_centerVertical="true"
android:layout_alignParentLeft="true"
android:layout_marginLeft="28px"
>
<TextView
tools:text="123456"
android:textAppearance="#style/citation_select_item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/citation_select_citation_number_text"/>
<TextView
tools:text="Pay by: Nov 18th, 2019"
android:textAppearance="#style/citation_select_item_due_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/citation_select_due_date_text"/>
<TextView
tools:text="a category label"
android:textAppearance="#style/citation_select_item_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/citation_select_category_text"/>
</LinearLayout>
<TextView
tools:text="$10.00"
android:textAppearance="#style/citation_select_item_cost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:id="#+id/citation_select_cost_text" android:layout_marginRight="28px"/>
</RelativeLayout>
<ImageView
android:src="#drawable/connector_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="#+id/citation_select_connector_left"/>
</LinearLayout>
The connector line far from the side that the view appears on is meant to disappear when onBindViewHolder is called, and the margins updated accordingly.
if (position % 2 == 0) {
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_left).visibility = View.GONE
val marginLayoutParams1 = holder.citationHolder.layoutParams as GridLayoutManager.LayoutParams
marginLayoutParams1.setMargins(0, 12, 12, 12)
holder.itemView.findViewById<LinearLayout>(R.id.citation_select_holder).layoutParams =
marginLayoutParams1
} else {
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_right).visibility = View.GONE
val marginLayoutParams2 = holder.citationHolder.layoutParams as GridLayoutManager.LayoutParams
marginLayoutParams2.setMargins(12, 12, 0, 12)
holder.itemView.findViewById<LinearLayout>(R.id.citation_select_holder).layoutParams =
marginLayoutParams2
}
Scrolling is done exclusively via on-screen buttons in increments of six. The first two pages load normally:
But the pattern begins to break down at citation #14. Keep in mind that the citation numbers correspond are the view's position within the RecyclerView:
What is happening to change the behavior?
I think I know what can help you fix this. I presume that it is reusing the old view, as a RecyclerView should and nowhere in your code is there a line to set the visibility of the connector lines back to visible.
You should add to both your GONE visibilities also the code to set the other to visible:
if (position % 2 == 0) {
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_right).visibility = View.VISIBLE
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_left).visibility = View.GONE
val marginLayoutParams1 = holder.citationHolder.layoutParams as GridLayoutManager.LayoutParams
marginLayoutParams1.setMargins(0, 12, 12, 12)
holder.itemView.findViewById<LinearLayout>(R.id.citation_select_holder).layoutParams =
marginLayoutParams1
} else {
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_left).visibility = View.VISIBLE
holder.itemView.findViewById<ImageView>(R.id.citation_select_connector_right).visibility = View.GONE
val marginLayoutParams2 = holder.citationHolder.layoutParams as GridLayoutManager.LayoutParams
marginLayoutParams2.setMargins(12, 12, 0, 12)
holder.itemView.findViewById<LinearLayout>(R.id.citation_select_holder).layoutParams =
marginLayoutParams2
}
Additional explanation
The RecyclerView will reuse some old view, right? Well since both lines are VISIBLE at the start, you assume that's their default state. But when you set the lines to GONE you never put them back to visible, and thus if RecyclerView reuses that view, it'll not add the margin there and it will just be missing the connector line. You always want to have EVERY line of code in onBindViewHolder to have a matching line that reverts it.
Vucko's answer is good, and the overall point (always update every component of your viewholder) is something you should absolutely do.
I wanted to add, however, that it appears as though you are not following the ViewHolder pattern correctly: your onBindViewHolder() method should never call findViewById(). Instead, your ViewHolder class should find each view once and then save references to them.
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val connectorRight: ImageView = itemView.findViewById(R.id.citation_select_connector_right)
val connectorLeft: ImageView = itemView.findViewById(R.id.citation_select_connector_left)
// ...
}
And then you can use these fields directly inside onBindViewHolder():
if (position % 2 == 0) {
holder.connectorRight.visibility = View.VISIBLE
holder.connectorLeft.visibility = View.GONE
// ...
} else {
holder.connectorLeft.visibility = View.VISIBLE
holder.connectorRight.visibility = View.GONE
// ...
}
I am creating a simple view where on the top I have some elements and below a recyclerView. When I scroll it down, would like to scroll the whole screen, not the only recycler.
I have achieved it with NestedScrollView, however, now the problem appears. Items in the list will be pretty heavy and in this configuration, all the items are bind at the same time(call of onBindViewHolder).
any ideas how to make them recycle and solve this problem?
Here is my xml file
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView 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"
android:background="#android:color/darker_gray"
android:orientation="vertical"
tools:context="com.gkuziel.testkotlin.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="#drawable/ic_available_stores_default" />
<TextView
android:id="#+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test text" />
<android.support.v7.widget.RecyclerView
android:id="#+id/list_test"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:isScrollContainer="false"
android:nestedScrollingEnabled="false">
</android.support.v7.widget.RecyclerView>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
Update:
The found a sweet solution: you add a complex header as ItemDecoration, its great cause your adapter can stay untouched, you just add sth like this:
recyclerView.addItemDecoration(dividerItemDecoration);
the only drawback of this solution is i couldn't make this header clickable (in my case it contains another recyclerView), however I know some people achieved it as well.
For this moment I decided implement heterogeneous recyclerview, with 1 instance of header type and the rest of simple row types.
What is important, the header type is fully binded once in HeaderViewHolder constructor and onBindViewHolder looks like this:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is HeaderViewHolder) {
//do nothing
Log.d("ProductAdapter", "Binding: Header")
} else if (holder is ItemViewHolder) {
Log.d("ProductAdapter", "Binding: " + position.toString())
val searchItem = items!![position - 1]
//here the proper binding is going on
}
}
You can try setting the recyclerview layout manager's method canScrollVertical to false and it won't respond to any touch inner scroll events.
override below method and return false.
boolean canScrollVertically()
here it is how to set.
#Override
protected void onCreate(Bundle savedInstanceState) {
// ...
// Lookup the recyclerview in activity layout
RecyclerView listTest = (RecyclerView) findViewById(R.id.list_test);
// Attach the adapter to the recyclerview to populate items
listTest.setAdapter(adapter);
// Set layout manager to position the items
listTest.setLayoutManager(new LinearLayoutManager(this){
#Override
public boolean canScrollVertically(){
return false;
}
});
// That's all!
}
Below is my BindableItem Class which also act as an adapter while using Groupie
class FilterByAthleteTypeItem(var athleteResponse: AthleteModel, var onFilterAthleteItemClick: OnFilterAthleteItemClick) : BindableItem<FilterItemLayoutBinding>() {
override fun bind(viewBinding: FilterItemLayoutBinding, position: Int) {
ViewHolder(viewBinding, position)
viewBinding.executePendingBindings()
viewBinding.notifyChange()
}
override fun getLayout(): Int {
return R.layout.filter_item_layout
}
inner class ViewHolder(var binding: FilterItemLayoutBinding, position: Int) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
override fun onClick(p0: View?) {
athleteResponse.isChecked = binding.playlistSwitch.isChecked
onFilterAthleteItemClick.onFilterAthleteClicked(athleteResponse)
notifyChanged()
}
init {
val athleteModel = athleteResponse
binding.totalItems.text = athleteModel.areelCount.toString()
binding.playlistSwitch.isChecked = athleteModel.isChecked
binding.sportName.text = athleteModel.athleteType
binding.playlistSwitch.setOnClickListener(this)
when {
athleteModel.athleteType == "highschool" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context, R.color.black))
athleteModel.athleteType == "college" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context, R.color
.college))
athleteModel.athleteType == "pro" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context, R.color.pro))
athleteModel.athleteType == "enthusiast" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context,
R.color.enthusiast))
athleteModel.athleteType == "military" -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context,
R.color.text_color_9b))
else -> binding.playerLevelImage.setBackgroundColor(
ContextCompat.getColor(binding.root.context,
R.color.white))
}
}
}
}
interface OnFilterAthleteItemClick {
fun onFilterAthleteClicked(athleteModel: AthleteModel)
}
Here is how I used it in MyActivity
Section section = new Section();
section.setHeader(headerItemGroupie);
if (!Utils.isNull(athleteModelList))
for (int i = 0; i < athleteModelList.size(); i++) {
AthleteModel athleteModel = athleteModelList.get(i);
athleteModel.setPosition(i);
athleteModelList.remove(i);
athleteModelList.add(i, athleteModel);
section.add(new FilterByAthleteTypeItem(athleteModelList.get(i), this));
}
groupAdapter.add(section);
Below is my layout Item file
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/white"
android:orientation="horizontal">
<com.areel.android.customview.CustomTextView
android:id="#+id/totalItems"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginStart="28dp"
android:layout_weight="1.7"
android:paddingBottom="18dp"
android:paddingTop="18dp"
android:textColor="#color/text_color_9b"
android:textSize="12sp"
app:fontPath="#string/font_avenir_heavy"
app:letterSpacing="0.154"
tools:text="14,932"/>
<com.areel.android.customview.CustomTextView
android:id="#+id/sportName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:gravity="end"
android:textAllCaps="true"
android:textColor="#color/black"
android:textSize="12sp"
app:fontPath="#string/font_avenir_heavy"
app:letterSpacing="0.3"
tools:text="NAME OF SPORT"/>
<FrameLayout
android:id="#+id/frameLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1.8">
<ImageView
android:id="#+id/playerLevelImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="21dp"
android:scaleType="fitXY"/>
<android.support.v7.widget.AppCompatCheckBox
android:id="#+id/playlistSwitch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:button="#drawable/switch_on_off"/>
</FrameLayout>
</LinearLayout>
and here is my recyclerView in layout
<android.support.v7.widget.RecyclerView
android:id="#+id/filter_list_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/showFriendLayout"
android:clipChildren="false"
android:clipToPadding="false">
</android.support.v7.widget.RecyclerView>
Now when I scroll the recyclerView the background color and Image start shuffling means respective positions of background color doesn't remain appropriate while scrolling
the major problem is the last item has an image as a background, and when I scroll that Image overlaps other backgrounds and shuffle happened
But there is now way to change that image to color so I need more robust solution!!
I am adding the screen shots here
First one, What I have done
And I check items and Scrolls the recyclerview then that last image shuffles its position like below image
The FIfth Image also shuffles on Down side
Author of the library here. You have several problems in your posted code.
For starters, you shouldn't be calling executePendingBindings or notifyChange. You aren't using the feature of data binding where you bind a model object, so both of those are unnecessary.
Second, the whole point of Groupie (especially with data binding) is that you shouldn't have to create your own ViewHolder. In fact, yours isn't doing anything. You can move all the code from your ViewHolder class into FilterByAthleteTypeItem.bind().
Last, your problem with images loading in the wrong spots, or duplicated, is a very common issue in RecyclerViews. You didn't post your image loading code, but I'm guessing it's asynchronously loading images -- meaning they load into a certain view, regardless of whether it's been recycled -- and/or failing to clear the old image from reused viewholders. I recommend you avoid dealing with this problem entirely by using an image loading library like Picasso or Glide.
Hope that helps!