AutoCompleteTextView dropdown height not wrapping to content - android

My app has an AutoCompleteTextView that queries a Room database and displays the search results in the dropdown as the user types.
The results are displayed correctly but the dropdown's height is always too large for the number of items (see screenshot below).
Basically the AutoCompleteTextView has a TextWatcher that sends the current text to the ViewModel, which in turn queries the database. Once the results are received, the ViewModel updates the view state (a Flow) and the fragment, which observes it, reassigns the AutoCompleteTextView's adapter with the new data.
Dropdown item layout
<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:id="#+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="#dimen/spacing_8">
<GridLayout
android:id="#+id/imageContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
... />
<com.google.android.material.textview.MaterialTextView
android:id="#+id/txtName"
style="#style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
... />
</androidx.constraintlayout.widget.ConstraintLayout>
Fragment layout
<com.google.android.material.textfield.TextInputLayout>
...
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="#+id/autoCompleteTextView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/spacing_16"/>
</com.google.android.material.textfield.TextInputLayout>
Adapter
internal class StationAdapter(
context: Context,
private val stations: List<UiStation>
) : ArrayAdapter<String>(
context,
R.layout.item_station,
stations.map { it.name }
) {
fun getStation(position: Int): UiStation = stations[position]
override fun getCount(): Int = stations.size
override fun getItem(position: Int): String = stations[position].name
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return convertView ?: run {
val inflater = LayoutInflater.from(context)
val binding = ItemStationBinding.inflate(inflater, parent, false).apply {
val station = stations[position]
imageContainer.addIconsForModes(station.modes)
txtName.text = station.name
}
binding.root
}
}
...
}
Fragment
private fun handleState(state: StationSearchViewState) = with(state) {
...
stations?.let {
val adapter = StationAdapter(
context = requireContext(),
stations = it
)
binding.autoCompleteTextView.setAdapter(adapter)
}
}
Now these are the other things I tried as well but didn't work (got the same result):
Passing all of the database table's results to the adapter and implementing a custom filter
Implementing a function to submit new data to the adapter and calling notifyDataSetChanged()
Setting the AutoCompleteTextView's android:dropDownHeight property to wrap_content
The only thing that seems to work is when I use an ArrayAdapter with the android.R.layout.simple_list_item_1 layout for the items

After 3 days working hard to figure this out I finally managed to find a solution, which is partly in this answer to an unrelated question.
Here is my code:
private const val DROPDOWN_ITEM_MAX_COUNT = 5
private const val PADDING = 34
class CustomAutoCompleteTextView(
context: Context,
attributeSet: AttributeSet?
) : MaterialAutoCompleteTextView(context, attributeSet) {
override fun onFilterComplete(count: Int) {
val itemCount = if (count > DROPDOWN_ITEM_MAX_COUNT) {
DROPDOWN_ITEM_MAX_COUNT
} else {
count
}
val individualItemHeight = (height / 2) + PADDING
dropDownHeight = itemCount * individualItemHeight
super.onFilterComplete(count)
}
}
Essentially I had to create a custom AutoCompleteTextView which, once the data is filtered, takes an item count (no greater than the maximum I've set) and multiplies it by the height of an individual item on my dropdown (a) plus a padding (b) to make sure the dropdown view always wraps the items.
a: I reached this value with a bit of trial and error but it still works across different screen sizes and densities
b: not mandatory. I just thought adding a bit of padding made it look nicer
End result

Related

Android Studio: autoSize of EditText doesn't work

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)

How to color text of RecyclerView item depending on a variable

lets say I have a variable
isBlue = true.
If it is true I want to set my newly added RecyclerView item text to blue.
If it is false I want to set my newly added RecyclerView item text to green.
if(isBlue){
dateTextView.setTextColor(ContextCompat.getColor(context!!, R.color.redColor))
}else{
dateTextView.setTextColor(ContextCompat.getColor(context!!, R.color.greenColor))
}
}
I have tried adding this code to onBind(), but it paints all the text to the set color, repainting the previous RecyclerView item texts, so I end up with all RecyclerView item text being either blue or green.
private inner class SubjectDateHolder(view:View) : RecyclerView.ViewHolder(view), View.OnClickListener{
private lateinit var date : Date
private val dateTextView :TextView = itemView.findViewById(R.id.text_view_subject_date)
fun bind(date : Date){
this.date = date
dateTextView.text = SimpleDateFormat("d.MMMM ''y. EEEE").format(this.date)
}
}
private inner class SubjectDateAdapter(var dates : List<Date>) : RecyclerView.Adapter<SubjectDateHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubjectDateHolder {
val view = layoutInflater.inflate(R.layout.list_item_date, parent, false)
return SubjectDateHolder(view)
}
override fun getItemCount(): Int {
return dates.size
}
override fun onBindViewHolder(holder: SubjectDateHolder, position: Int) {
val date = dates[position]
holder.bind(date)
}
}
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#ffffff">
<TextView
android:id="#+id/text_view_subject_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:textSize="20sp"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
If I understood correctly from the commentary, you want to add items to your recyclerview and want to color the new item. To do that, instead of passing a listOf<Date> to your recyclerview you need to pass a listOf<MyItem> data class with something like this:
data class MyItem(val isBlue: Boolean, val date: Date)
In doing so when you're in the holder.bind() method you can use your condition to color the text
So you should be add one boolean key in your API according to color isBlue or isGreen so when you get Json you can write conditional code for color of text like..
if(<Your Boolean Key of Json>){
dateTextView.setTextColor(ContextCompat.getColor(context!!, R.color.redColor))
}else{
dateTextView.setTextColor(ContextCompat.getColor(context!!, R.color.greenColor))
}
}

UI lags and is choppy when using Glide to load images in a RecyclerView

I have a RecyclerView that loads images from URLs using Glide. Now the URLs are retrieved from Firebase using pagination as you can see below. The issue is that when the MainActivity (which contains the below code and the recyclerview) is first initialized there is a substantial lag in the UI (very laggy and choppy scrolling, options menu takes 3 seconds to open etc.) and the images take a while to load. After i scroll down though and reach the end of the RecyclerView for the first page of data, the OnScrollListener is triggered and i start loading new data from a new query. I've tried my best to optimize what Glide does based on suggestions from a user on another post i made and i also set the adapter.setHasFixedSize to true without luck. Any idea what's happening here? Am i hanging the UI thread somehow despite the queries being async?
EDIT
: Could Glide be causing the lag on the main Thread due to it having to load multiple images into the recycler view's imageViews? And if so, what can i do to counter that?
Here's how i handle the pagination of the data i get from Firebase and notify the adapter:
class MainActivity : AppCompatActivity() {
private val TAG: String = MainActivity::class.java.simpleName // Tag used for debugging
private var queryLimit : Long = 50 // how many documents should the query request from firebase
private lateinit var iconsRCV : RecyclerView // card icons recycler view
private lateinit var lastVisible:DocumentSnapshot
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val rootRef: FirebaseFirestore = FirebaseFirestore.getInstance()
val urlsRef : CollectionReference = rootRef.collection("CardIconUrls")
val query : Query = urlsRef.orderBy("resID",Query.Direction.ASCENDING).limit(queryLimit) // create a query for the first queryLimit documents in the urlsRef collection
// Setting Toolbar default settings
val toolbar : Toolbar = findViewById(R.id.mainToolbar)
setSupportActionBar(toolbar) // set the custom toolbar as the support action bar
supportActionBar?.setDisplayShowTitleEnabled(false) // remove the default action bar title
// RecyclerView initializations
iconsRCV = findViewById(R.id.cardIconsRCV)
iconsRCV.layoutManager = GridLayoutManager(this,5) // set the layout manager for the rcv
val iconUrls : ArrayList<String> = ArrayList() // initialize the data with an empty array list
val adapter = CardIconAdapter(this,iconUrls) // initialize the adapter for the recyclerview
iconsRCV.adapter = adapter // set the adapter
iconsRCV.setHasFixedSize(true)
iconsRCV.addOnScrollListener(object:OnScrollListener(){
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if(!iconsRCV.canScrollVertically(1) && (newState == RecyclerView.SCROLL_STATE_IDLE) && ((iconsRCV.layoutManager as GridLayoutManager).findLastVisibleItemPosition() == (iconsRCV.layoutManager as GridLayoutManager).itemCount-1)) {
Log.d(TAG,"End of rcv-Starting query")
val nextQuery = urlsRef.orderBy("resID",Query.Direction.ASCENDING).startAfter(lastVisible).limit(queryLimit).get().addOnCompleteListener { task ->
if(task.isSuccessful) {
Log.d(TAG,"Next query called")
for(document:DocumentSnapshot in task.result!!) {
iconUrls.add(document.get("url").toString())
}
lastVisible = task.result!!.documents[task.result!!.size()-1]
adapter.notifyDataSetChanged()
}
}
}
}
})
query.get().addOnCompleteListener {task: Task<QuerySnapshot> ->
if(task.isSuccessful) {
Log.d(TAG,"Success")
for(document:DocumentSnapshot in task.result!!) {
Log.d(TAG,"Task size = " + task.result!!.size())
iconUrls.add(document.get("url").toString()) // add the url to the list
}
lastVisible = task.result!!.documents[task.result!!.size()-1]
adapter.notifyDataSetChanged() // notify the adapter about the new data
}
}
}
Here's the recyclerview adapter:
public class CardIconAdapter extends RecyclerView.Adapter<CardIconAdapter.ViewHolder> {
private List<String> urlsList;
private Context context;
class ViewHolder extends RecyclerView.ViewHolder {
ImageView iconImg;
ViewHolder(#NonNull View view) {
super(view);
iconImg = view.findViewById(R.id.cardIcon);
}
}
public CardIconAdapter(Context cntxt, List<String> data) {
context = cntxt;
urlsList = data;
}
#NonNull
#Override
public CardIconAdapter.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_icons_rcv_item,parent,false);
return new ViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull CardIconAdapter.ViewHolder holder, int position) {
RequestOptions requestOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL);
GlideApp.with(context).load(urlsList.get(position)).thumbnail(0.25f).centerCrop().dontTransform().apply(requestOptions).into(holder.iconImg);
}
#Override
public int getItemCount() {
return urlsList.size();
}
}
I finally figured out the issue after a lot of trial and error. My first mistake was not posting my xml layout file for the recyclerview item because that was the source of the performance issues. The second mistake was that I was using a LinearLayout and had set its own layout_width and layout_height attributes to 75dp instead of the ImageView's which is nested inside of the LinearLayout and was using wrap_content for the ImageView's respective attributes. So to fix the performance issues i did the following :
I changed the LinearLayout to a ConstraintLayout (i read that it is much more optimized in general)
I set the ConstraintLayout's layout_width & layout_height attributes to wrap_content and finally
I set the actual ImageView's layout_width & layout_height attributes to 75dp which is the actual size that i want the image to be
Here's the final layout file for each item of the recyclerview after the changes :
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal"
android:padding="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/cardIcon"
android:layout_width="75dp"
android:layout_height="75dp"
android:contentDescription="cardIcon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="#tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>
Some of the ways to fix lagging when loading large number of images
Setting fixed size true to your RecyclerView but it wont help you if pagination involves
mRecyclerView.setHasFixedSize(true);
Overriding pixels of the image using glide like this
Glide.with(this)
.load(YOUR_URL)
.apply(new RequestOptions().override(dimenInPx, dimenInPx)
.placeholder(R.drawable.placeHolder).error(R.drawable.error_image))
.into(imageview);
Add hardwareAccelerated="true" property to your activity tag inside manifest like
<activity
...
android:hardwareAccelerated="true"
...
/>
I think this may help.
Try to use paging library combined with recyclerview
link reference:
Paging Library for Android With Kotlin: Creating Infinite Lists
Android Jetpack: manage infinite lists with RecyclerView and Paging (Google I/O '18)
This answer from Github was very helpful
Basically, if you set the exact dimensions of your image, it stops Glide from constantly having to figure out how big the image needs to be, and significantly improves performance. This helped take my RecyclerView from unusable to perfectly fine, just by setting a defined height and width on the ImageViews I was loading.
Trust me. Don't use ConstraintLayout as in the accepted answer. It's performance is low when compared to the LinearLayout. According to my measurements, it took average of 16 ms to inflate a layout file with ConstraintLayout and 10 ms for a layout file with LinearLayout. To have smooth scroll effect, it has to inflate layout at least within 16 ms.
In a Glide sample app here, they have created a SquareImageView which returns same width and height.
class SquareImageView #JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) :
AppCompatImageView(context, attrs, defStyleAttr) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
}
}
Don't forget to declare styleable in attrs.xml
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.example.SquareImageView
android:id="#+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="#dimen/padding_normal"
android:scaleType="centerCrop" />
<TextView
android:id="#+id/text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="#dimen/padding_small"
android:textAlignment="center"
android:textColor="#color/secondaryTextColor"
android:textSize="15sp" />
</LinearLayout>
Try to avoid ConstraintLayout in the RecyclerView as it can cause some lag when inflating ViewHolders for the first time.
Another option will be to pre-inflate some views and use them in onCreateViewHolder
init {
val layoutInflater = LayoutInflater.from(context)
mPreInflated = List(20) {
MyLayoutBinding.inflate(layoutInflater, null, false)
}.iterator()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = if (mPreInflated.hasNext()) {
mPreInflated.next()
} else {
val layoutInflater = LayoutInflater.from(context)
MyLayoutBinding.inflate(layoutInflater, null, false)
}
return MyViewHolder(binding)
}

Android expandable cardView for existing recyclerView adapter

Alrighty, So I have an existing custom recycler view adapter that populates a recycler view using provided items, and sets attributes in a layout i have as follows, ill try to remove irrelevant items from code
class TransactionAdapter(val context: Context, var transactions: List<Transaction>) :
androidx.recyclerview.widget.RecyclerView.Adapter<TransactionAdapter.CustomViewHolder>() {
override fun onCreateViewHolder(p0: ViewGroup, p1: Int): CustomViewHolder {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val view = inflater.inflate(R.layout.transaction_list_inner_view, p0, false)
return CustomViewHolder(view)
}
override fun onBindViewHolder(p0: CustomViewHolder, p1: Int) {
p0.transactionNameTextView?.text = transactions[p1].title
p0.transactionAmountTextView?.text = transactions[p1].amount
if (!transactions[p1].location.isNullOrEmpty()) {
p0.transactionLocationTextView?.text = transactions[p1].location
} else {
p0.transactionLocationTextView?.text = "N/A"
}
p0.transactionTimeTextView?.text = transactions[p1].createdAt
p0.transactionDeleteButton?.setOnClickListener { println("working delete") }
}
class CustomViewHolder(v: View) : androidx.recyclerview.widget.RecyclerView.ViewHolder(v) {
val transactionNameTextView: TextView? = v.findViewById(R.id.transactionNameTextView) as TextView?
val transactionAmountTextView: TextView? = v.findViewById(R.id.transactionAmountTextView) as TextView?
val transactionLocationTextView: TextView? = v.findViewById(R.id.transactionLocationTextView) as TextView?
val transactionTimeTextView: TextView? = v.findViewById(R.id.transactionTimeTextView) as TextView?
val transactionDeleteButton: AppCompatButton? = v.findViewById(R.id.transactionDeleteButton) as AppCompatButton?
}
}
Now, I want to implement this library to use instead expandable card views so that I can only show for example the name and amount of transactions, and have the rest be expanded, this library can be found here https://github.com/AleSpero/ExpandableCardView , the library asks me to make a new layout with an inner_view attribute, I have done so, my layout is called transaction_list_expandable_view.xml and it looks like this
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:ignore="ExtraText">
<com.alespero.expandablecardview.ExpandableCardView
android:id="#+id/transaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:title="testt"
app:inner_view="#layout/transaction_list_inner_view"
app:expandOnClick="true"
app:animationDuration="300"
app:startExpanded="false"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
Now comes the problem, my custom TransactionAdapter only handles one layout, which is the one I called transaction_list_inner_view, how can I have this adapter instead handle both inner and expandable views and get the desired result? (a list of cards with relevant titles that expands to reveal the rest of the details belonging to them)
Sorry for the long question and code, thanks in advance for any help.
After checking the code of the library you're using, i think that you shouldn't be inflating the inner view manually (in your adapter) as that's the responsibility of the ExpandableCardView you should inflate the transaction_list_expandable_view.xml in your adapter.
It would look like :
val view = inflater.inflate(R.layout.transaction_list_expandable_view, p0, false)
For populating your inner view, i'm not sure whether the inner view is already inflated in the expandable one at the time of instantiating the CustomViewHolder.
if it's the case just the switching of inner => expandable views above should do the trick.

How to visually stay on same scroll position, upon unknown number of RecyclerView data changes (insertion, deletion, updates)?

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.

Categories

Resources