Creating recycler view item animation - android

I am trying to creating a animation in a recyclerview. The kind of animation that I am looking for is the one which is available in Telegram android app.
When you open a chat in telegram and long press on the message, the recyclerview multi select option comes with checkbox. I am trying to create the same effect.
My current status:
ListItemAdapter.kt
class ListItemAdapter(private val values: List<PlaceholderContent.PlaceholderItem>
) : RecyclerView.Adapter<ListItemAdapter.ItemViewHolder>() {
private lateinit var itemClick: OnItemClick
private var selectedIndex: Int = -1
private var selectedItems: SparseBooleanArray = SparseBooleanArray()
private var isActive: Boolean = false
fun setItemClick(itemClick: OnItemClick) {
this.itemClick = itemClick
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ListItemAdapter.ItemViewHolder {
val view = LayoutInflater.from(parent.context).inflate(
R.layout.fragment_item,
parent,
false
)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ListItemAdapter.ItemViewHolder, position: Int) {
holder.itemView.apply {
findViewById<TextView>(R.id.item_number).text = values[position].id
findViewById<TextView>(R.id.content).text = values[position].content
}
holder.itemView.setOnClickListener {
itemClick.onItemClick(values[position], position)
}
holder.itemView.setOnLongClickListener {
itemClick.onLongPress(values[position], position)
true
}
toggleIcon(holder, position)
}
override fun getItemCount(): Int {
return values.size
}
fun toggleIcon(holder: ItemViewHolder, position: Int){
val checkBox = holder.itemView.findViewById<RadioButton>(R.id.is_selected)
if(selectedItems.get(position, false)){
checkBox.isGone = false
checkBox.isChecked = true
}
else{
checkBox.isGone = true
checkBox.isChecked = false
}
if(isActive) checkBox.isGone = false
if(selectedIndex == position) selectedIndex = - 1
}
fun selectedItemCount() = selectedItems.size()
fun toggleSelection(position: Int){
selectedIndex = position
if (selectedItems.get(position, false)){
selectedItems.delete(position)
}else {
selectedItems.put(position, true)
}
notifyItemChanged(position)
isActive = selectedItems.isNotEmpty()
notifyDataSetChanged()
}
fun clearSelection(){
selectedItems.clear()
notifyDataSetChanged()
}
interface OnItemClick {
fun onItemClick(item: PlaceholderContent.PlaceholderItem, position: Int)
fun onLongPress(item: PlaceholderContent.PlaceholderItem, position: Int)
}
inner class ItemViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
}
}
fragment_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="#+id/list_item"
>
<RadioButton
android:id="#+id/is_selected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_margin="16dp"
/>
<TextView
android:id="#+id/item_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
<TextView
android:id="#+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="#dimen/text_margin"
android:textAppearance="?attr/textAppearanceListItem" />
</LinearLayout>
MainActivity.kt
adapter = ListItemAdapter(PlaceholderContent.ITEMS)
val recyclerViewList = view.findViewById<RecyclerView>(R.id.list)
recyclerViewList.adapter = adapter
recyclerViewList.layoutManager = LinearLayoutManager(view.context, LinearLayoutManager.VERTICAL, false)
val myHelper = ItemTouchHelper(myCallback)
myHelper.attachToRecyclerView(recyclerViewList)
adapter.setItemClick(object : ListItemAdapter.OnItemClick{
override fun onItemClick(
item: PlaceholderContent.PlaceholderItem,
position: Int
) {
if(adapter.selectedItemCount() > 0)
toggleSelection(position)
}
override fun onLongPress(
item: PlaceholderContent.PlaceholderItem,
position: Int
) {
toggleSelection(position)
}
})
return view
}
private fun toggleSelection(position: Int){
adapter.toggleSelection(position)
}

To add an animated transition, the simplest way is by adding
animateLayoutChanges="true"
to the individual item root(since that's where you'd like it animated), which in your case would be:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:id="#+id/list_item"
animateLayoutChanges="true" <!-- Around here -->
>
...
</LinearLayout>
To have it look exactly like in telegram, you'd need a bit of a different design to start with, but that's another issue(and a matter of perseverance in a way :P);
Also if you'd like different types of animations, you'd need to go at it with a different way, you could do it by adding animate to the view: ( alpha goes from 0f to 1f usually((invisible-visible)), and you'd need to change the view visibility paremeter if needed to visible/invisible/gone so that it doesn't register click events on that area, depending on your needs. In any case, this should be trial & error to get your desired behavior)
is_selected.animate().alpha(1f).setInterpolator(AccelerateDecelerateInterpolator())
is_selected.visibility = View.VISIBLE
You could also read up a little on available android interpolators, since we can't be quite sure whether they will remove some(unlikely)/add new ones.

Related

Can't show items by sections correctly in recyclerview in kotlin

I am relatively new in kotlin and I would like to know how I can display items by sections, like this: showing data in sections . I have two services, one returns the list of section names and the other returns the items for each section.
So far I did the following, the view is like this:
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools">
<TextView
android:id="#+id/header_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="header" />
<androidx.appcompat.widget.SwitchCompat
android:id="#+id/header_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list_productos_seccion"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="#+id/header_switch"
app:layout_constraintStart_toStartOf="#+id/header_title"
app:layout_constraintTop_toBottomOf="#+id/header_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
and in my outer adapter I did the following, where I pass a list of sections to the adapter and for each one I consult the service to obtain items and display it:
class SeccionesAdapter(
var seccionClickAdapter: SeccionClickAdapter,
private val context: Context,
var fragment: Fragment,
var productoViewModel: ProductoViewModel,
var vm: ClientActivityViewModel
): RecyclerView.Adapter<SeccionesAdapter.MyViewHolder>() {
interface SeccionClickAdapter{
fun onSeccionClick(producto: ProductoQryDTO)
}
var dataList = emptyList<SeccionQryDTO>()
var productosLista = ArrayList<ProductoQryDTO>()
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun setData(seccionDTO: SeccionQryDTO) {
itemView.header_title.text = seccionDTO.nombre
itemView.header_switch.isChecked = seccionDTO.isSelected?: true
itemView.list_productos_seccion.layoutManager = LinearLayoutManager(context)
if (itemView.header_switch.isChecked) {
itemView.list_productos_seccion.visibility = View.VISIBLE
dataFromServer(seccionDTO.id, itemView)
}
itemView.header_switch.setOnCheckedChangeListener { _, isChecked -> visible(isChecked, seccionDTO.id) }
}
fun visible(isChecked: Boolean, idSeccionDTO: Int) {
if (isChecked) {
itemView.list_productos_seccion.visibility = View.VISIBLE
dataFromServer(idSeccionDTO, itemView)
} else {
itemView.list_productos_seccion.visibility = View.GONE
}
}
}
fun dataFromServer(idSeccionDTO: Int, itemView: View) {
productosLista.clear()
productoViewModel.getItems(
idSeccionDTO,
{
fragment.activity?.runOnUiThread {
productosLista.clear()
productosLista.addAll(it.data!!)
itemView.list_productos_seccion.adapter = ProductoxSeccionAdapter(context, productosLista, seccionClickAdapter)
}
}, {
Log.e("Error", "", it)
}
)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item_header_model, parent, false))
}
override fun getItemCount(): Int {
return dataList.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.setData(dataList[position]!!)
}
fun setSecciones(seccionesList : List<SeccionQryDTO>) {
this.dataList = seccionesList
notifyDataSetChanged()
}
}
The problem is that when the application is loaded, sometimes the amount of items that should be for each section is not shown, and when I scroll it begins to make calls to the service, also the elements disappear and it remains blank as shown in this image
blank space in sections.
Could you help me please? I don't know how I could handle this situation or if I should implement it in some other way
EDIT
This is my inner adapter: ProductoxSeccionAdapter
class ProductoxSeccionAdapter (
private val context: Context,
var listProductos: List<ProductoQryDTO>?,
var seccionClickAdapter: SeccionesAdapter.SeccionClickAdapter
) : RecyclerView.Adapter<ProductoxSeccionAdapter.MyViewHolder>() {
inner class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun setData(producto: ProductoQryDTO) {
itemView.nombreProductoLista.text = producto.nombre
itemView.descProductoLista.text = producto.detalle
itemView.precioProductoLista.text = producto.simboloMoneda + producto.precio
itemView.setOnClickListener { seccionClickAdapter.onSeccionClick(producto) }
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(context).inflate(R.layout.item_producto_cliente_lista, parent, false))
}
override fun getItemCount(): Int {
return listProductos!!.size
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.setData(listProductos!![position])
}
}

RecyclerView list item view not updating (using DiffUtil.ItemCallback)

SOLVED
RV - recycler view
I have an RV inside an alertdialog. Adapter for the RV extends ListAdapter with DiffUtil.ItemCallback. List for the adapter is being updated every 500ms using countdowntimer (checking whether the list item is downloaded or not).
The problem is, the list is updated and submitted to the adapter with the new data and but the list item view is not updating based on new data provided as shown below. I'm using data/view binding for updating the list item view.
The RV sometimes updates the item view when being scrolled.
PS: The RV is a child of NestedScrollView
This is how it is working right now
Adapter code
class AlarmSongsAdapter(
private val onItemClicked: (AlarmSongItem) -> Unit,
private val startDownloading: (String) -> Unit,
private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : ListAdapter<AlarmSongItem, AlarmSongsAdapter.AlarmSongsViewHolder>(DiffUtilCallback) {
object DiffUtilCallback : DiffUtil.ItemCallback<AlarmSongItem>() {
override fun areItemsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean {
return oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AlarmSongsViewHolder {
return AlarmSongsViewHolder(AlarmsSongListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), onItemClicked, startDownloading, insertDownloadEntityInDB)
}
override fun onBindViewHolder(holder: AlarmSongsViewHolder, position: Int) {
holder.bind(getItem(position))
}
class AlarmSongsViewHolder(
private val binding: AlarmsSongListItemBinding,
private val onItemClicked: (AlarmSongItem) -> Unit,
private val startDownloading: (String) -> Unit,
private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(alarmSongItem: AlarmSongItem) {
binding.alarmSongItem = alarmSongItem
binding.executePendingBindings()
}
init {
binding.downloadButton.setOnClickListener {
val alarmSongItem = binding.alarmSongItem!!
when(alarmSongItem.downloadState){
Download.STATE_STOPPED -> {
startDownloading(alarmSongItem.audioFile)
val storageInfo = StorageUtils.currentStorageTypeAndPath(binding.root.context)
insertDownloadEntityInDB(alarmSongItem.toDownloadEntity(storageInfo))
}
else -> {}
}
}
binding.root.setOnClickListener {
onItemClicked(binding.alarmSongItem!!)
}
}
}
}
List item view code
<?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">
<data>
<variable
name="alarmSongItem"
type="com.baja.app.domain.models.AlarmSongItem" />
</data>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:cardElevation="5dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp">
<androidx.cardview.widget.CardView
android:id="#+id/song_item_thumbnail_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardBackgroundColor="#android:color/transparent"
app:cardCornerRadius="6dp"
app:cardElevation="0dp">
<ImageView
android:id="#+id/song_item_thumbnail"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_centerVertical="true"
android:scaleType="centerCrop"
app:srcCompat="#drawable/bg_default_light"
tools:ignore="ContentDescription"
app:thumbnailFromUri="#{alarmSongItem.thumbnail}" />
</androidx.cardview.widget.CardView>
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="60dp"
android:id="#+id/download_progress_container"
android:layout_alignParentRight="true"
android:layout_centerVertical="true">
<ImageView
android:id="#+id/download_bg"
android:layout_width="32dp"
android:layout_height="32dp"
android:scaleType="centerCrop"
app:srcCompat="?bg_default_circular"
tools:ignore="ContentDescription"
android:layout_centerInParent="true" />
<com.google.android.material.button.MaterialButton
android:id="#+id/download_button"
style="#style/AppTheme.OutlinedButton.Icon"
android:layout_width="32dp"
android:layout_height="32dp"
app:cornerRadius="32dp"
app:icon="#drawable/ic_download"
app:iconTint="#android:color/white"
changeIcon="#{alarmSongItem.downloadState}"
android:layout_centerInParent="true" />
<com.google.android.material.progressindicator.ProgressIndicator
android:id="#+id/download_progress_bar"
style="#style/Widget.MaterialComponents.ProgressIndicator.Circular.Indeterminate"
android:layout_width="33dp"
android:layout_height="33dp"
app:circularRadius="17dp"
app:indicatorColor="?attr/progressIndicatorColor"
app:indicatorWidth="1dp"
showProgressBar="#{alarmSongItem.downloadState}"
android:layout_centerInParent="true"
android:visibility="gone" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_marginStart="20dp"
android:layout_toEndOf="#id/song_item_thumbnail_container"
android:orientation="vertical"
android:weightSum="2"
android:layout_toStartOf="#id/download_progress_container"
android:layout_marginEnd="8dp">
<TextView
android:id="#+id/song_item_name"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:ellipsize="end"
android:gravity="bottom"
android:maxLines="1"
android:textSize="16sp"
android:textStyle="bold"
tools:text="Sa re ga ma pa"
android:text="#{alarmSongItem.title}" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
android:id="#+id/song_item_artist"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxWidth="150dp"
android:maxLines="1"
android:textSize="14sp"
tools:text="Sidharth Arun"
android:text="#{alarmSongItem.artist}" />
<View
android:layout_width="5dp"
android:layout_height="5dp"
android:layout_gravity="center_vertical"
android:background="#drawable/dot" />
<TextView
android:id="#+id/song_item_duration"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
tools:text="10:12"
app:formatDuration="#{alarmSongItem.duration}" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
Binding Adapter functions
#BindingAdapter("thumbnailFromUri")
fun thumbnailFromUri(view: ImageView, uri: String) {
Glide.with(view).load(uri).placeholder(R.drawable.bg_default_light).error(R.drawable.bg_default_light).into(view)
}
#BindingAdapter("changeIcon")
fun changeIconBasedOnDownloadState(view: MaterialButton, state: Int) {
when (state) {
Download.STATE_COMPLETED -> view.setIconResource(R.drawable.ic_check)
else -> view.setIconResource(R.drawable.ic_download)
}
}
#BindingAdapter("showProgressBar")
fun showProgressbarBasedOnState(view: ProgressIndicator, state: Int) {
when (state) {
Download.STATE_QUEUED,
Download.STATE_RESTARTING,
Download.STATE_DOWNLOADING -> view.visibility = View.VISIBLE
else -> view.visibility = View.GONE
}
}
The video was extremely helpful in pinpointing the probelm.
Your Diffutils "areContentsTheSame()" is checking the item not an individual property of the item. When the file is downloaded you need to have "areContentsTheSame()" check the download property to tell if there was a change in the specific property.
example
class MyDiffCallback : DiffUtil.ItemCallback<Dev>() {
...
override fun areContentsTheSame(oldItem: Dev, newItem: Dev): Boolean {
return oldItem.downloadStatus == newItem.download.status &&
oldItem == newItem
}
}
The problem is, the list is updated and submitted to the adapter with the new data and but the list item view is not updating based on new data provided as shown below. I'm using data/view binding for updating the list item view.
This happens because you're submitting the same list to your submitList() you can take a look at this post for more information
I've had the same issue recently and I've been able to resolve it quite easily by using onBindViewHolder(holder: AlarmSongsViewHolder, position: Int, payloads: MutableList<Any>)
In your DiffUtilCallback:
const val BUNDLE_TIME = "bundle_time"
object DiffUtilCallback : DiffUtil.ItemCallback<AlarmSongItem>() {
override fun areItemsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: AlarmSongItem, newItem: AlarmSongItem): Boolean = false
// This will be called every time you submit a list (so every 500ms)
override fun getChangePayload(oldItem: AlarmSongItem, newItem: AlarmSongItem): Any {
val diffBundle = Bundle()
// pass the data you want to update
diffBundle.putLong(BUNDLE_TIME, newItem.time)
return diffBundle
}
}
and then in your adapter override:
note that there is payloads:MutableList at the end
override fun onBindViewHolder(holder: AlarmSongsViewHolder, position: Int, payloads: MutableList<Any>) {
if(payloads.isEmpty()) {
// if empty it's a new item that appears on the screen
super.onBindViewHolder(holder, position, payloads)
return
}
payloads.forEach { when(it) {
is Bundle -> {
val time = it.getLong(BUNDLE_TIME)
holder.binding.alarmSongItem.time.text = time.toString()
}
}}
}
You can even create a function in your ViewHolder to pass the data to update if you don't want to expose binding
class AlarmSongsViewHolder(
private val binding: AlarmsSongListItemBinding,
private val onItemClicked: (AlarmSongItem) -> Unit,
private val startDownloading: (String) -> Unit,
private val insertDownloadEntityInDB: (DownloadEntity) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun bind(alarmSongItem: AlarmSongItem) {
binding.alarmSongItem = alarmSongItem
binding.executePendingBindings()
}
fun updateMyItem(time: Long) {
binding.alarmSongItem.time.text = time.toString()
}
}
You need to add a method to your Adapter which will be called when list is updated
class AlarmSongsAdapter(alarmSongItems: List<AlarmSongItem>) : RecyclerView.Adapter<AlarmSongsAdapter.ViewHolder>() {
private val mAlarmSongItems = mutableListOf<AlarmSongItem>()
init {
mAlarmSongItems.addAll(alarmSongItems)
}
fun swap(alarmSongItems: List<AlarmSongItem>) {
val diffCallback = DiffUtilCallback(this.mAlarmSongItems, alarmSongItems)
val diffResult = DiffUtil.calculateDiff(diffCallback)
this.mAlarmSongItems.clear()
this.mAlarmSongItems.addAll(alarmSongItems)
diffResult.dispatchUpdatesTo(this)
}
}
mAlarmSongItems is the initial list you passed to your adapter (Sorry, I didn't copy all your variables, just the one needed to show difference meaningful)
Your callback,
class DiffUtilCallback(
private val oldList: List<AlarmSongItem>,
private val newList: List<AlarmSongItem>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}
}
So now wherever your receive your updated countdowntimer values, where you initialized your adapter, you could do
alarmAdapter.swap(updatedAlarmValues)
Remove changeIconBasedOnDownloadState and put the code in bind().
Assuming AlarmSongItem.downloadState has a different value in the new list, this is what you need to do.
Move your Binding Adapter code to bind()
fun bind(alarmSongItem: AlarmSongItem) {
binding.alarmSongItem = alarmSongItem
binding.executePendingBindings()
when (alarmSongItem.downloadState) {
Download.STATE_QUEUED,
Download.STATE_RESTARTING,
Download.STATE_DOWNLOADING -> view.visibility = View.VISIBLE
else -> view.visibility = View.GONE
}
}
Your issue: the ViewHolder is not updated when you update the data
There are three ways to trigger a ViewHolder to reload the content:
you scroll it out of the screen and back in -> since it is a recyclerView it will reuse the ViewHolder for another Item and when you scroll back up, it will reload the first items -> updated the img
when you notify the adapter that an item changed
using DiffUtils and reload the ViewHolder by calling diffResult.dispatchUpdatesTo(adapter)
First options seems to be working for you!
For the second:
If you call adapter.notifyDataSetChanged() it will reload all ViewHolders.
If you call adapter.notifyItemChanged(int position) it will reload a specific item position.
To understand the origin of your issue, you might want to try this to just see whether the issue lies deeper.
For the third:
Please show the code where you calculate the DiffUtil result.
I've solved the issue (in my way). Thanks for all the answers, they are really helpful, but not in my usecase.
Solution:
To make it work as accurately, I created another variable for download details(like state, percentage etc) in my rv_list_item.xml and passed the respective download.
DiffUtil is now working perfectly.

How to stop a view in RecyclerView from updating its value when it has focus?

I have a RecyclerView with a set of CardView that contain an EditText in each card. The RecyclerView adapter receives a list of numbers every second and it binds the data to each CardView's EditText.
However, when the user click on one of the EditView I want that EditView to stop changing every second while the EditView has the focus, so the user can enter their own data.
I don't really know what to do if (editView.hasFocus)
Adapter
class ConverterRecyclerViewAdapter(val ratesList: ArrayList<Currency>,
val context: Context?,
val onRateListener: OnRateListener)
: RecyclerView.Adapter<ConverterRecyclerViewAdapter.ViewHolder>() {
class ViewHolder(itemView: View, val onRateListener: OnRateListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener{
var editText: EditText
init{
itemView.setOnClickListener(this)
editText = itemView.findViewById(R.id.value_conv_cv)
}
fun bindItem(currency : Currency){
itemView.currency_name_conv_cv.text = currency.currencyName
itemView.country_currency_name_conv_cv.text = currency.countryCurrencyName
itemView.value_conv_cv.text = SpannableStringBuilder(currency.value.toString())
itemView.flag_iv_conv_cv.setImageResource(currency.flag)
itemView.tag = currency.currencyName
}
override fun onClick(v: View?) {
onRateListener.onRateClick(adapterPosition, itemView.currency_name_conv_cv.text.toString())
}
}
interface OnRateListener {
fun onRateClick(position : Int, currency : String)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.converter_card_layout, parent, false)
return ViewHolder(view, onRateListener)
}
override fun getItemCount(): Int {
return ratesList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindItem(ratesList[position])
}
}
CardView.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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"
app:cardElevation="0dp"
app:cardUseCompatPadding="true">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<ImageView
android:id="#+id/flag_iv_conv_cv"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="#mipmap/ic_launcher_round"/>
<TextView
android:id="#+id/currency_name_conv_cv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CURRENCY"
android:textStyle="bold"
android:textSize="16sp"
android:paddingTop="#dimen/list_item_tv_padding"
android:paddingStart="#dimen/list_item_tv_padding"
android:layout_toRightOf="#id/flag_iv_conv_cv"
android:layout_toEndOf="#+id/flag_iv_conv_cv"
android:textColor="#android:color/black"/>
<TextView
android:id="#+id/country_currency_name_conv_cv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="#id/flag_iv_conv_cv"
android:layout_toEndOf="#+id/flag_iv_conv_cv"
android:layout_below="#+id/currency_name_conv_cv"
android:paddingStart="#dimen/list_item_tv_padding"
android:text="Country Currency Name"
android:textSize="12sp"
android:textColor="#android:color/darker_gray"/>
<EditText
android:id="#+id/value_conv_cv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignEnd="#id/country_currency_name_conv_cv"
android:gravity="right"
android:text="100"
android:textSize="24sp" />
</RelativeLayout>
</androidx.cardview.widget.CardView>
Fragment
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this).get(ConverterViewModel::class.java)
viewModel.initialisePositions()
viewModel.setupTimer(viewModel.ratesPositions[0])
val ratesLiveData = viewModel.rates
ratesLiveData.observe(viewLifecycleOwner, Observer { data ->
refreshData(data.rates)
viewModel.sortListByPositionsArray(ratesArray)
val firstElementInRecyclerView = viewModel.ratesPositions[0]
val view = recyclerView.findViewWithTag<View>(firstElementInRecyclerView)
lateinit var editText : EditText
if (converterAdapter == null) setUpRecyclerView()
else {
converterAdapter!!.notifyDataSetChanged()
}
})
}
First of all, you want to update your item when you don't have the focus on it, so indeed you have to use if (!editView.hasFocus()) {}
Then if you have to check if your list ratesArray is holding the same items or not, because if you do ratesArray.clear() at some point, you're clearing the objects then the RecyclerView will create a new ViewHolder for your item.
If you are clearing and creating new objects continuously, you have to "pair" your Adapter with the items.
For example, if you always clear the items and create them again respecting the position, add this method to your Adapter:
override fun getItemId(position: Int): Long {
return position.toLong()
}
you can relate the "rate" in any other ways (a String inside the object, for example). If you do it with the position, you're pairing each item with their order.
Now, to finish "respecting that order", you have to tell the adapter that it has stable ids. That's done with Adapter.setHasStableIds(true)
Maybe like this:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if(!holder.editText.hasFocus()){
holder.bindItem(ratesList[position])
}
}
UPDATE
remove the focusable attribute for edittext and add this to its xml
android:focusableInTouchMode="false"
in the adapter:
//decalare clicked item as member of the class and set it to -1
var clickedItem = -1;
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if(clickedItem != position){
holder.bindItem(ratesList[position])
}
//on click
holder.editText.setOnClickListener {
clickedItem = position;
notifyDataSetChanged();
}
}
Your EditText needs special binding so remove this line from fun bindItem():
itemView.value_conv_cv.text = SpannableStringBuilder(currency.value.toString())
And on onBindViewHolder, bind this view only when it's not focused, which requires listening. Try this:
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val current = ratesList[position]
holder.bindItem(current)
holder.value_conv_cv.setOnFocusChangeListener { view, hasFocus ->
if (hasFocus) {
// do nothing
} else {
holder.value_conv_cv.text = SpannableStringBuilder(current.value.toString())
}
}
}

Android add background view below recyclerview in every 10th item (like tumblr)

I am trying to make a recycler view like tumblr app. You can see here: https://streamable.com/s/gpyec/kxvjnz
My question is how to add a video (or any clickable) below recyclerview? I added an item decoration implementation as follows:
class RecyclerViewAdItemDecoration(private val func:() -> Unit) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildLayoutPosition(view)
val mLayoutManager = parent.layoutManager
if (mLayoutManager is GridLayoutManager) {
setGridParams(view, position, parent)
} else if (mLayoutManager is LinearLayoutManager) {
setLinearParams(view, position, parent)
}
}
private fun setGridParams(view: View, position: Int, parent: RecyclerView) {
val p = view.layoutParams as ViewGroup.MarginLayoutParams
if (position == 0) {
p.setMargins(0,0,0, 0)
} else if (position >= 10 && (position % 10 == 0 || position % 11 == 0)) {
p.setMargins(0,0,0, parent.height)
func()
} else {
p.setMargins(0,0,0, 0)
}
}
private fun setLinearParams(view: View, position: Int, parent: RecyclerView) {
val p = view.layoutParams as ViewGroup.MarginLayoutParams
if (position == 0) {
p.setMargins(0,0,0, 0)
} else if (position >= 10 && (position % 10 == 0)) {
p.setMargins(0,0,0, parent.height)
func()
} else {
p.setMargins(0,0,0, 0)
}
}
}
This way I could add enough space for background view but it's not clickable now. I also couldn't find any library for such implementation. Appreciated for any help.
Edit:
To clarify, I want to show background video (or any view) right after every 10th item in recycler view. Like it's seen in video in the link, there is a space between every 10 item in recycler view, which also triggers to play the video in the background (below recycler view)
show background video (or any view) right after every 10th item in
recycler view.
If the background video is after every 10th item, it means there is an item (11th), which is transparent.
What you actually want here is recyclerview with multiple view types.
Use a RelativeLayout for activity_main.xml which allows to place views
on top of others (in Z axis).
ex: RecyclerView is the top most view here.
<RelativeLayout 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"
tools:context=".MainActivity"
android:focusable="true">
<View
android:id="#+id/ad"
android:background="#color/colorAccent"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_posts"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</RelativeLayout>
Create two item layouts for two types of recycler view types
ex:
item_normal.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="100dp">
<TextView
android:id="#+id/tv_post"
tools:text="Post"
android:background="#android:color/white"
android:textSize="32sp"
android:gravity="center_horizontal|center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
item_transparent.xml (where layout background is transparent that allow to see the view below the surface area)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/transparent"/>
Setting android:clickable=false in item_transparent does not stop triggering the click event on transparent item, so use communication flow using interfaces, to bring the other view(ad) to the front when clicked on transparent item.
MainActivity.kt
class MainActivity : AppCompatActivity(), RvAdpater.OnItemClick {
private lateinit var adView: View
private lateinit var rvPosts: RecyclerView
override fun onClick() {
bringAdFront()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
rvPosts = findViewById(R.id.rv_posts)
rvPosts.layoutManager = LinearLayoutManager(this)
val rvAdpater = RvAdpater()
rvAdpater.setListener(this)
rvPosts.adapter = rvAdpater
}
private fun bringAdFront() {
adView = findViewById<View>(R.id.ad)
adView.bringToFront()
}
override fun onBackPressed() {
// to go back to the normal recycler view when back button is pressed
val parent = rvPosts.parent as ViewGroup
parent.removeAllViews()
parent.addView(adView, 0)
parent.addView(rvPosts, 1)
}
}
RvAdapter.kt
const val TAG = "RvAdpater"
class RvAdpater : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private lateinit var listener:OnItemClick
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewNormal = LayoutInflater.from(parent.context).inflate(R.layout.item_normal, parent, false)
val viewTransparent = LayoutInflater.from(parent.context).inflate(R.layout.item_transparent, parent, false)
return when(viewType){
0 -> NormalViewHolder(viewNormal)
2 -> TransparentViewHolder(viewTransparent)
else -> NormalViewHolder(viewNormal)
}
}
override fun getItemCount(): Int = 10
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(getItemViewType(position)){
0 -> {
val normalHolder = holder as NormalViewHolder
normalHolder.tv.text = "Post"
normalHolder.itemView.setOnClickListener {
Log.d(TAG, "Clicked on Normal item")
}
}
2 -> {
val transparentHolder = holder as TransparentViewHolder
transparentHolder.itemView.setOnClickListener {
listener.onClick()
}
}
}
}
fun setListener(onItem:OnItemClick){
listener = onItem
}
interface OnItemClick{
fun onClick()
}
override fun getItemViewType(position: Int): Int = position % 2 * 2
class NormalViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
val tv:TextView = itemView.findViewById(R.id.tv_post)
}
class TransparentViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
}
Checkout the repo for an working example
For handling multiple view types, you may use Epoxy library.

setOnLongClickListener in android with kotlin

How can I use setOnItemClickListner in each item in my ListView?
my xml :
<ListView
android:id="#+id/tv1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</ListView>
this my adapter class
inner class mo3d1Adapter : BaseAdapter {
override fun getItemId(p0: Int): Long {
return p0.toLong()
}
override fun getCount(): Int {
return listOfmo3d.size
}
var listOfMkabala = ArrayList<MeetingDetails>()
var context: Context? = null
constructor(context: Context, listOfMkabaln: ArrayList<MeetingDetails>) : super() {
this.listOfMkabala = listOfMkabaln
this.context = context
}
override fun getView(p0: Int, p1: View?, p2: ViewGroup?): View {
val mo3d = listOfmo3d[p0]
var inflatormo3d = context!!.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
var myViewmo3d = inflatormo3d.inflate(R.layout.fragment_item, null)
lvMo3d.onItemClickListener = AdapterView.OnItemClickListener { adapterView, view, i, l ->
Toast.makeText(context, " TEST STACK ", Toast.LENGTH_LONG).show()
}
myViewmo3d.meeting_name.text = mo3d.name1!!
myViewmo3d.meeting_date.text = mo3d.date.toString()!!
myViewmo3d.attendance_number.text = mo3d.n2.toString()!!
return myViewmo3d
}
override fun getItem(p0: Int): Any {
return listOfmo3d[p0]
}
}
I want listener for each item in my ListView
And when I used this method setOnClickListener in adapter it's not working, where can I use?
Try this in your activity class
lv.setOnItemClickListener { parent, view, position, id ->
Toast.makeText(this, "Position Clicked:"+" "+position,Toast.LENGTH_SHORT).show()
}
Although a little quirky this works fine for me.
latestMessagesAdapter.setOnItemLongClickListener { item, view ->
val row = item as LatestMessageRow
return#setOnItemLongClickListener(true)
}
First of all I would like to tell that it is RecyclerView rather than ListView. You can find plenty information why to do in such. For example you can read it hear :
RecyclerView vs. ListView
Regarding your question how to do it in correct way with RecyclerView.
Insert dependencies with RecyclerView, they are now in support library in Kotlin.
implementation "com.android.support:appcompat-v7:25.4.0"
First change your ListView with RecyclerView in xml layout like this:
<android.support.v7.widget.RecyclerView
android:id="#+id/accountList"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
Create Adapter for RecyclerView:
class AccountListAdapter(val accountList: AccountList, val itemListener: (Account) -> Unit) :
RecyclerView.Adapter<AccountListAdapter.ViewHolder>(){
override fun getItemCount(): Int = accountList.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) =
holder.bind(accountList[position])
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder{
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_account, parent, false)
return ViewHolder(view, itemListener)
}
class ViewHolder(itemView: View, val itemClick: (Account) -> Unit): RecyclerView.ViewHolder(itemView){
fun bind(account : Account){
with(account){
itemView.accountName.text = title
itemView.setOnClickListener{ itemClick(this)}
}
}
}
}
item_account.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/accountName"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
Models (in Kotlin you can put them in one file and name for example AccountModels.kt) :
data class AccountList(val accounts: List<Account>){
val size : Int
get() = accounts.size
operator fun get(position: Int) = accounts[position]
}
data class Account(val id : Long, val title : String, val balance : Int, val defCurrency: Int)
In Fragment/Activity connect your Adapter to RecyclerView:
override fun onStart() {
super.onStart()
setupAdapter()
}
fun setupAdapter(){
Log.d(TAG, "updating ui..")
val account1 = Account(1,"Credit", 1000, 2)
val account2 = Account(2, "Debit", 500, 2)
val account3 = Account(3, "Cash", 7000, 2)
val accounts : List<Account> = listOf(account1, account2, account3)
val adapter = AccountListAdapter(AccountList(accounts)){
val title = it.title
Log.d(TAG, "$title clicked")
}
accountList.layoutManager = LinearLayoutManager(activity)
accountList.adapter = adapter
}
That is all. Everything should work now. Hope it helps.

Categories

Resources