Having an odd issue with a RecyclerView within in a ViewPager. Adapter is a PagerAdapter that has fixed size of two and returns pre-inflated ViewGroups. One of which has a RecyclerView in it.
Cannot get my head around it. Any help would be much appreciated.
On inflating the view do I ened to set the root? or possibly attachToRoot?
Sliding works fine on all but touching the RecyclerView.
Support version 23.3.0 but was still an issue on previous versions, so don't believe that is it.
xml that contains the RecyclerView. The InterceptingLayout is just a RelativeLayout in this case, with option of intercepting touch events, but isn't the issue here.
<xxxx.layouts.InterceptingRelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/list_interceptor"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.malinskiy.superrecyclerview.SuperRecyclerView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_empty="#layout/layout_empty"
app:layout_moreProgress="#layout/layout_empty"
app:mainLayoutId="#layout/recyclerview_list"
app:recyclerPaddingTop="#dimen/status_bar_toolbar_height"/>
</xxx.layouts.InterceptingRelativeLayout>
Inflation of ViewGroup that contains RecyclerView
listInterceptor = (getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater).inflate(R.layout.stub_list_content, null) as InterceptingRelativeLayout
And relevant methods in Pager Adapter
override fun instantiateItem(container: ViewGroup, position: Int): Any {
if (position == 0) {
container.addView(daypicker)
return daypicker
} else {
container.addView(listInterceptor)
return listInterceptor
}
override fun getItemPosition(obj: Any?) = POSITION_NONE
override fun getCount() = 2
override fun isViewFromObject(arg0: View, arg1: Any): Boolean {
return arg0 === arg1
}
Related
in my current project there's an EditText with fixed layout_width and layout_height, called exercise that is expanded downwards programmatically: One line of text (String) + "\n" is added to the EditText.
Sometimes the line added to the EditText, let's call it element, is too long to fit inside the full width of the object so it's splitted into a new line.
The thing is I would either like the lines' text size in exercise to be resized to fit the EditText's width or have a clear visible distance between each line (element), but just not inside the newline due to not fitting inside the exercise's width.
Therefore I googled as much as I could and tried out every possible solution I could find today.
What I tried:
Using either EditText as the object and android:autoSizeTextType="uniform" & android:inputType="textMultiLine|textCapSentences"as attributes or androidx.appcompat.widget.AppCompatEditText, accompanied by the attributes app:autoSizeMaxTextSize="28sp", app:autoSizeMinTextSize="8sp"and app:autoSizeStepGranularity="1sp"
(with a device that supports just exactly API 26)
using other types of text objects
using lineSpacingExtra to insert some spacing. This unfortunately also inserted the spacing between the wrapped/ splitted line so the original element's line that got splitted by wrapping inside the EditText had the spacing aswell.
That's where I am now. I can't get the text size be reduced automatically when the line would be too wide for the EditText's width.
I could supply the full XML, if needed.
I'm grateful for any hint that could help here. Thanks in advance!
Here's a really basic RecyclerView implementation (using view binding, let me know if you're not familiar with that - you can just findViewById all the things instead):
class MainFragment : Fragment(R.layout.fragment_main) {
lateinit var binding: FragmentMainBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentMainBinding.bind(view)
with(binding) {
val adapter = MyAdapter()
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(requireContext())
addButton.setOnClickListener {
val item = textEntry.text.toString()
if (item.isNotBlank()) {
adapter.addItem(item)
textEntry.text.clear()
}
}
}
}
}
class MyAdapter : RecyclerView.Adapter<MyAdapter.ViewHolder>() {
private var data: List<String> = emptyList()
fun addItem(item: String) {
data = data + item
notifyItemInserted(data.lastIndex)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = ItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.textView.text = data[position]
}
override fun getItemCount(): Int = data.size
class ViewHolder(val binding: ItemViewBinding) : RecyclerView.ViewHolder(binding.root)
}
fragment_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:gravity="center"
android:padding="10dp">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/textEntry"
/>
<EditText
android:id="#+id/textEntry"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:singleLine="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="#id/addButton"
/>
<Button
android:id="#+id/addButton"
android:text="ADD"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
item_view.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
>
<TextView
android:id="#+id/textView"
app:autoSizeTextType="uniform"
android:layout_width="match_parent"
android:layout_height="48sp"
android:maxLines="1"
android:gravity="center_vertical"
/>
</FrameLayout>
It's pretty simple - you have a text entry field and a button to add the contents as a new line. The button passes the contents to addItem on the adapter, which appends it to the list of lines in data. The RecyclerView just displays all the items in data, using a ViewHolder layout that has an auto-sizing TextView to scale each item.
Ideally you'd want to persist data somehow (e.g. the Add button passes the new data to a ViewModel, stores it somehow, updates the current list which the adapter has observed so it updates whenever there's a change) - I just left it as a basic proof of concept. Also, it's easier to store separate items if they're kept separate - you can always serialise it by joining them into a single string if you really want! But generally you wouldn't want to do that
edit - since you're having trouble with the setTypeface thing, this is all it is:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
with(holder.binding.textView) {
val styled = position % 2 == 0
text = data[position]
setTypeface(typeface, if (styled) Typeface.BOLD else Typeface.NORMAL)
setTextColor(if (styled) Color.RED else Color.BLACK)
}
}
The logic is just styling alternate items differently, but hopefully you get the idea. You decide how to style a given item depending on what it is, and then you apply that style by setting attributes as appropriate. It's always an "if this is true do A, otherwise do B" situation, so you're always setting the attribute one way or the other. You never only set it for one case, because then you're leaving old state displayed if it's not that case.
It's more complicated, but you also have the option of creating different ViewHolders (with their own XML layouts) for different kinds of item. So instead of having a single ViewHolder that has to work with everything, where you have to reconfigure things like all the styling in onBindViewHolder depending on which type of item is displayed, you can just have different ViewHolders with different styling, different layouts etc:
// creating a sealed class so we can say our adapter handles a MyViewHolder type,
// and we can have a specific set of possible subclasses of that
sealed class MyViewHolder(view: View) : RecyclerView.ViewHolder(view)
class HeaderViewHolder(val binding: HeaderItemBinding) : MyViewHolder(binding.root)
class ItemViewHolder(val binding: ItemViewBinding) : MyViewHolder(binding.root)
// the Adapter now uses the MyViewHolder type (which as above, covers a couple of different
// ViewHolder classes we're using)
class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
private var data: List<String> = emptyList()
fun addItem(item: String) {
data = data + item
notifyItemInserted(data.lastIndex)
}
// some identifiers for the different ViewHolder types we're using
private val HEADER_TYPE = 0
private val ITEM_TYPE = 1
override fun getItemViewType(position: Int): Int {
// Work out what kind of ViewHolder the item in this position should display in.
// This gets passed to onCreateViewHolder, where you create the appropriate type,
// and that type of ViewHolder is what gets passed into onBindViewHolder for this position
return if (data[position].startsWith("Section")) HEADER_TYPE else ITEM_TYPE
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val inflater = LayoutInflater.from(parent.context)
// creating the appropriate ViewHolder instance depending on the type requested
return when(viewType) {
HEADER_TYPE -> HeaderViewHolder(HeaderItemBinding.inflate(inflater, parent, false))
ITEM_TYPE -> ItemViewHolder(ItemViewBinding.inflate(inflater, parent, false))
else -> throw RuntimeException("Unhanded view type!")
}
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
// The type of MyViewHolder passed in here depends on what getItemViewType returns
// for this position - binding is a different type in each case,
// because it's been generated from different layouts
when(holder) {
is HeaderViewHolder -> holder.binding.textView.text = data[position]
is ItemViewHolder -> holder.binding.textView.text = data[position]
}
}
override fun getItemCount(): Int = data.size
}
(You could be a bit more clever than this, but just to illustrate the general idea!)
That's using the same item_view.xml as before, and a header_item.xml variation on that (but you could have literally anything, they're completely separate layouts, completely separate ViewHolders):
header_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
>
<TextView
android:id="#+id/textView"
app:autoSizeTextType="uniform"
android:layout_width="match_parent"
android:layout_height="48sp"
android:layout_weight="1"
android:textStyle="bold"
android:textColor="#DD1100"
android:maxLines="1"
android:gravity="center_vertical"
/>
</LinearLayout>
So instead of having to "redesign" one ViewHolder in code to go back and forth between different item types and their styling, You can just use two differently-styled layouts. It's a bit more work to set up, but it can be neater and much more flexible when you have completely independent things - especially if you want to give them different functionality. It depends whether it's worth it to you, or if you're happy to just have a single ViewHolder and restyle things in code, hide or show elements etc.
you can try something like this
if(et.getText().length()>10) {
et.setTextSize(newValue)
Consider an ArrayList which contains 4 element. The RecylcerView shows 4 CardViews.
I know how to do that. For this particular RecyclerView, I want a different behaviour: there's a 5th cardview for adding new items. After new item is succesfully added to the ArrayList, then the "add item" CardView will be the 6th, etc.
Here's what I already have:
override fun getItemViewType(position: Int): Int {
if (position < itemCount) return VIEW_DISPLAY_ITEM
else return VIEW_ADD_ITEM
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : RecyclerView.ViewHolder{
if (viewType == VIEW_DISPLAY_ITEM){
return ViewHolderDisplayItem(
LayoutInflater.from(context).inflate(R.layout.display_item, parent, false)
)
}
else {
return ViewHolderAddItem(
LayoutInflater.from(context).inflate(R.layout.add_new_item, parent, false)
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType){
VIEW_DISPLAY_ITEM -> {
(holder as ViewHolderDisplayItem).bind(position)
holder.itemView.setOnClickListener {
val theData = theList.get(position)
txtItemName.text = theData.name
txtItemID.text = theData.id
txtItemLocation.text = theData.location
}
}
VIEW_ADD_ITEM -> {
(holder as ViewHolderAddItem).bind(position)
holder.itemView.setOnClickListener {
}
}
}
}
The result is not what I expected. The "add new item" cardview is not shown. I guess that's because when getItemType() is invoked, position is always lesser than itemCount.
No idea what's the simple solution for this. Hint/sample code is appreciated.
Not included in your question is the code for getItemCount(). Make sure you are returning list.size + 1 in order to include that final "add new item" element.
You try
override fun getItemViewType(position: Int): Int {
if (position <= itemCount) return VIEW_DISPLAY_ITEM
else return VIEW_ADD_ITEM }
I think you have a little misunderstanding in overriding the getItemViewType() function, position start from 0.
Hope it
If u need simple solution
you can just use this CardView outside the recycler view but make sure that recycler view is wrap content so that it can be expandable
ScrollView >> LinearLayout >> RecyclerView >> CardView
Just like that
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/course_names_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"/>
So u can easily add new item to your adapter without using view types or something else
I’ve already made it and it works fine with me
I'm trying to make a transition with simple animation of shared element between Fragments. In the first fragment I have elements in RecyclerView, in second - exactly the same element (defined in separate xml layout, in the list elements are also of this type) on top and details in the rest of the view. I'm giving various transitionNames for all elements in bindViewHolder and in onCreateView of target fragment I'm reading them and set them to element I want make transition. Anyway animation is not happening and I don't have any other ideas. Here below I'm putting my code snippets from source and target fragments and list adapter:
ListAdapter:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = list[position]
ViewCompat.setTransitionName(holder.view, item.id)
holder.view.setOnClickListener {
listener?.onItemSelected(item, holder.view)
}
...
}
interface interactionListener {
fun onItemSelected(item: ItemData, view: View)
}
ListFragment (Source):
override fun onItemSelected(item: ItemData, view: View) {
val action = ListFragmentDirections.itemDetailAction(item.id)
val extras = FragmentNavigatorExtras(view to view.transitionName)
val data = Bundle()
data.putString("itemId", item.id)
findNavController().navigate(action.actionId, data, null, extras)
}
SourceFragmentLayout:
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/pullToRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="#layout/item_overview_row" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
DetailFragment (Target):
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.fragment_detail, container, false)
val itemId = ItemDetailFragmentArgs.fromBundle(arguments).itemId
(rootView.findViewById(R.id.includeDetails) as View).transitionName = itemId
sharedElementEnterTransition = ChangeBounds().apply {
duration = 750
}
sharedElementReturnTransition= ChangeBounds().apply {
duration = 750
}
return rootView
}
DetailFragmentLayout:
<include
android:id="#+id/includeDetails"
layout="#layout/item_overview_row"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
ItemOverviewRowLayout (this one included as item in recyclerView and in target fragment as header):
<androidx.cardview.widget.CardView 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="wrap_content"
android:clickable="true"
android:focusable="true"
android:foreground="?android:attr/selectableItemBackground"
android:orientation="vertical" >
I made also another application using Jetpack navigation, shared elements and elements described by the same layout.xml and it's working since I'm not making transition from recyclerView to target fragment. Maybe I'm wrong here, setting the transitionName to found view in target fragment? I don't know how to make it another way, because the IDs of target included layout should be unique because of recyclerView items.
Okay, I found that how should it looks like to have enter animation with shared element:
In DetailFragment (Target) you should run postponeEnterTransition() on start onViewCreated (my code from onCreateView can be moved to onViewCreated). Now you have time to sign target view element with transitionName. After you end with loading data and view, you HAVE TO run startPostponedEnterTransition(). If you don't do it, ui would freeze, so you can't do time consuming operations between postponeEnterTransition and startPostponedEnterTransition.
Anyway, now the problem is with return transition. Because of course it's the same situation - you have to reload recyclerView before you release animation. Of course you can also use postponeEnterTransition (even if it's return transition). In my case, I have list wrapped by LiveData. In source fragment lifecycle observer is checking data. There is another challenge - how to determine if data is loaded. Theoretically with recyclerView you can use helpful inline function:
inline fun <T : View> T.afterMeasure(crossinline f: T.() -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
}
})
}
...and in code where you are applying your layout manager and adapter you can use it like this:
recyclerView.afterMeasure { startPostponedEnterTransition() }
it should do the work with determine time when return animation should start (you have to be sure if transitionNames are correct in recyclerView items so transition can have target view item)
From the answer that using ViewTreeObserver is quite consume resources a lot. and also have a lot of processes to do. so I do suggest you use doOnPreDraw instead of waiting after recyclerView was measured. the code implement will like this below.
recyclerView.doOnPreDraw {
startPostponedEnterTransition()
}
I have implemented a RecyclerView using the GridLayoutManager and an adpter, containing multiple item types with different viewholders of different heights.
Unfortunately, the RecyclerView is lagging when scrolling it. I have observed this behaviour:
On a specific low-end device (tablet), which is in a very poor condition and really slow in general, I could see a lot of lagging when I am scrolling up and down.
On a high-end device (Samsung Galaxy S8), I see a lag on the first swipe (scroll) from the top consistently (no exceptions)
I think the different heights are the cause of this problem and I have tried a lot of different approaches. Among these are:
setting the height of each item manually (so no wrap_content for heights!)
Setting setHasStableIds(true) and implementing this in the adapter:
override fun getItemId(position: Int): Long {
return position.toLong() }
During nothing in onBindViewHolder and only relying on static content in the viewholders, defined in the xml layouts
Removing everthing related to drawables, other graphics and fonts in the xml layouts. So only text and basic layouts like LinearLayout (etc.) was left.
A lot of other minor adjustments..
I will try and give you the relevant parts of my code.
So first, here is the xml of my screen with the recyclerview:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/customersRecyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="50dp"
android:paddingBottom="50dp"
android:layout_weight="1"
android:clipToPadding="false">
</android.support.v7.widget.RecyclerView>
</FrameLayout>
And here is the code for creating the adapter:
var columns = if (isBigScreen && isLandscape) 6
else if (isBigScreen) 5
else if (isLandscape) 4
else 3
val gridLayoutManager = GridLayoutManager(context, columns, GridLayoutManager.VERTICAL, false)
mCustomersRecyclerview?.layoutManager = gridLayoutManager
mCustomersAdapter = CustomersAdapter(customersListItems, gridLayoutManager, columns)
mCustomersRecyclerview?.adapter = mCustomersAdapter
And the adapter constructor:
constructor(items: ArrayList<ListItem> = ArrayList(), layoutManager: GridLayoutManager, spanSize: Int) {
mItems = items
mSpanSize = spanSize
mLayoutManager = layoutManager
setHasStableIds(true)
mLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
when (getItemViewType(position)) {
ItemType.HEADLINE_PRIMARY.index,
ItemType.HEADLINE_SECONDARY.index -> {
return spanSize
}
}
return 1
}
}
}
And an example of a headline viewholder, which uses span to cover the width fully in the list:
Part of the onCreateViewHolder(...)
when (viewType) {
ItemType.HEADLINE_PRIMARY.index -> {
layoutView = prepareViewHolder(parent, R.layout.customers_list_item_headline_primary, false)
if (layoutView != null) {
viewholder = HeadlinePrimaryViewHolder(layoutView)
}
}
And onBindViewHolder(...)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = mItems.get(position)
if (holder is HeadlinePrimaryViewHolder && item is HeadlinePrimaryListItem) {
holder.bind(item)
And the viewholder itself
class HeadlinePrimaryViewHolder: RecyclerView.ViewHolder {
private var mCustomersPrimaryHeadline: MTTextView? = null
constructor(view: View?) : super(view) {
mCustomersPrimaryHeadline = view as MTTextView
}
fun bind(itemPrimary: HeadlinePrimaryListItem) {
var headline: String = itemPrimary.headline
mCustomersPrimaryHeadline?.setText(headline)
}
}
(The XML of this viewholder is a simple textview and nothing else)
It is scrolling smoothly when..
The other viewholders besides the headlines, uses only 1 column (1 span) and looks different and has different heights as mentioned before.
The only thing that had a positive effect on the performance was using the same items/views, or at least items with the same height for the entire list. Both with and without setting setHasFixedSize(true). It does not matter which of my item types I use, as long as they have the same height all the way down.
But that is not an option in my case, as I need items to have different heights for my customized items.
Background
In case your RecyclerView gets new items, it is best to use notifyItemRangeInserted, together with unique, stable id for each item, so that it will animate nicely, and without changing what you see too much:
As you can see, the item "0", which is the first on the list, stays on the same spot when I add more items before of it, as if nothing has changed.
The problem
This is a great solution, which will fit for other cases too, when you insert items anywhere else.
However, it doesn't fit all cases. Sometimes, all I get from outside, is : "here's a new list of items, some are new, some are the same, some have updated/removed" .
Because of this, I can't use notifyItemRangeInserted anymore, because I don't have the knowledge of how many were added.
Problem is, if I use notifyDataSetChanged, the scrolling changes, because the amount of items before the current one have changed.
This means that the items that you look at currently will be visually shifted aside:
As you can see now, when I add more items before the first one, they push it down.
I want that the currently viewable items will stay as much as they can, with priority of the one at the top ("0" in this case).
To the user, he won't notice anything above the current items, except for some possible end cases (removed current items and those after, or updated current ones in some way). It would look as if I used notifyItemRangeInserted.
What I've tried
I tried to save the current scroll state or position, and restore it afterward, as shown here, but none of the solutions there had fixed this.
Here's the POC project I've made to try it all:
class MainActivity : AppCompatActivity() {
val listItems = ArrayList<ListItemData>()
var idGenerator = 0L
var dataGenerator = 0
class ListItemData(val data: Int, val id: Long)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val inflater = LayoutInflater.from(this#MainActivity)
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
return object : RecyclerView.ViewHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false)) {}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
val textView = holder!!.itemView as TextView
val item = listItems[position]
textView.text = "item: ${item.data}"
}
override fun getItemId(position: Int): Long = listItems[position].id
override fun getItemCount(): Int = listItems.size
}
adapter.setHasStableIds(true)
recyclerView.adapter = adapter
for (i in 1..30)
listItems.add(ListItemData(dataGenerator++, idGenerator++))
addItemsFromTopButton.setOnClickListener {
for (i in 1..5) {
listItems.add(0, ListItemData(dataGenerator++, idGenerator++))
}
//this is a good insertion, when we know how many items were added
adapter.notifyItemRangeInserted(0, 5)
//this is a bad insertion, when we don't know how many items were added
// adapter.notifyDataSetChanged()
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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"
tools:context="com.example.user.recyclerviewadditionwithoutscrollingtest.MainActivity">
<Button
android:id="#+id/addItemsFromTopButton" android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginRight="8dp"
android:text="add items to top" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="#+id/recyclerView"/>
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView" android:layout_width="0dp" android:layout_height="0dp"
android:layout_marginBottom="8dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
android:layout_marginTop="8dp" android:orientation="vertical"
app:layoutManager="android.support.v7.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
The question
Is it possible to notify the adapter of various changes, yet let it stay on the exact same place?
Items that are viewed currently would stay if they can, or removed/updated as needed.
Of course, the items' ids will stay unique and stable, but sadly the cells size might be different from one another.
EDIT: I've found a partial solution. It works by getting which view is at the top, get its item (saved it inside the viewHolder) and tries to scroll to it. There are multiple issues with this though:
If the item was removed, I will have to somehow scroll to the next one, and so on. I think in the real app, I can manage to do it. Wonder if there is a better way though.
Currently it goes over the list to get the item, but maybe in the real app I can optimize it.
Since it just scrolls to the item, if puts it at the top edge of the RecyclerView, so if you've scrolled a bit to show it partially, it will move a bit:
Here's the new code :
class MainActivity : AppCompatActivity() {
val listItems = ArrayList<ListItemData>()
var idGenerator = 0L
var dataGenerator = 0
class ListItemData(val data: Int, val id: Long)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val adapter = object : RecyclerView.Adapter<ViewHolder>() {
val inflater = LayoutInflater.from(this#MainActivity)
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
return ViewHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val textView = holder.itemView as TextView
val item = listItems[position]
textView.text = "item: ${item.data}"
holder.listItem = item
}
override fun getItemId(position: Int): Long = listItems[position].id
override fun getItemCount(): Int = listItems.size
}
adapter.setHasStableIds(true)
recyclerView.adapter = adapter
for (i in 1..30)
listItems.add(ListItemData(dataGenerator++, idGenerator++))
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
addItemsFromTopButton.setOnClickListener {
for (i in 1..5) {
listItems.add(0, ListItemData(dataGenerator++, idGenerator++))
}
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
val holder = recyclerView.findViewHolderForAdapterPosition(firstVisibleItemPosition) as ViewHolder
adapter.notifyDataSetChanged()
val listItemToGoTo = holder.listItem
for (i in 0..listItems.size) {
val cur = listItems[i]
if (listItemToGoTo === cur) {
layoutManager.scrollToPositionWithOffset(i, 0)
break
}
}
//TODO think what to do if the item wasn't found
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var listItem: ListItemData? = null
}
}
I would solve this problem using the DiffUtil api. DiffUtil is meant to take in a "before" and "after" list (that can be as similar or as different as you want) and will compute for you the various insertions, removals, etc that you would need to notify the adapter of.
The biggest, and nearly only, challenge in using DiffUtil is in defining your DiffUtil.Callback to use. For your proof-of-concept app, I think things will be quite easy. Please excuse the Java code; I know you posted originally in Kotlin but I'm not nearly as comfortable with Kotlin as I am with Java.
Here's a callback that I think works with your app:
private static class MyCallback extends DiffUtil.Callback {
private List<ListItemData> oldItems;
private List<ListItemData> newItems;
#Override
public int getOldListSize() {
return oldItems.size();
}
#Override
public int getNewListSize() {
return newItems.size();
}
#Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).id == newItems.get(newItemPosition).id;
}
#Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return oldItems.get(oldItemPosition).data == newItems.get(newItemPosition).data;
}
}
And here's how you'd use it in your app (in java/kotlin pseudocode):
addItemsFromTopButton.setOnClickListener {
MyCallback callback = new MyCallback();
callback.oldItems = new ArrayList<>(listItems);
// modify listItems however you want... add, delete, shuffle, etc
callback.newItems = new ArrayList<>(listItems);
DiffUtil.calculateDiff(callback).dispatchUpdatesTo(adapter);
}
I made my own little app to test this out: each button press would add 20 items, shuffle the list, and then delete 10 items. Here's what I observed:
When the first visible item in the "before" list also existed in the "after" list...
When there were enough items after it to fill the screen, it stayed in place.
When there were not, the RecyclerView scrolled to the bottom
When the first visible item in the "before" list did not also exist int he "after" list, the RecyclerView would try to keep whichever item that did exist in both "before" + "after" and was closest to the first visible position in the "before" list in the same position, following the same rules as above.