MotionLayout not recyclable as a child of RecyclerView - android

i try to implement programmatically version of MotionLayout by extending it. And i have a base activity ayout using RecyclerView.
However, when i add my motion layout as an item of the RecyclerView, the view is not recycled when i try to scrolling up and down.
And it works well when i use as a normal view (act as single view).
Here is the preview:
class SimpleMotionLayout #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MotionLayout(context, attrs, defStyleAttr) {
private val motionScene = MotionScene(this)
private var _simpleTransition: MotionScene.Transition? = null
private lateinit var squareView: View
init {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
initDefaultConstraint(this)
setMotion()
}
fun setMotion() {
_simpleTransition = createPlaceholderTransition(motionScene)
setDebugMode(DEBUG_SHOW_PATH)
/**
* The order matters here.
* [MotionScene.addTransition] adds the transition to the scene while
* [MotionScene.setTransition] sets the transition to be the current transition.
*/
motionScene.addTransition(_simpleTransition)
motionScene.setTransition(_simpleTransition)
scene = motionScene
setTransition(_simpleTransition!!.id)
animateView()
}
fun setSquareColor(color: Int) {
squareView.setBackgroundColor(color)
}
fun initDefaultConstraint(motionLayout: ConstraintLayout) {
// View
squareView = View(context).apply {
id = R.id.default_button
setBackgroundColor(Color.BLACK)
}
motionLayout.addView(
squareView,
LayoutParams(
fromDp(context, 52),
fromDp(context, 52)
)
)
val set = ConstraintSet()
set.clone(motionLayout)
// Setup constraint set to TOP, LEFT to the Parent
set.connect(
squareView.id,
TOP,
PARENT_ID,
TOP
)
set.connect(
squareView.id,
START,
PARENT_ID,
START
)
set.applyTo(motionLayout)
}
private fun setToEnd() {
val endSet = getConstraintSet(_simpleTransition?.endConstraintSetId ?: return)
endSet.clear(R.id.default_button, START)
endSet.connect(
R.id.default_button,
END,
PARENT_ID,
END
)
}
fun animateView() {
setToEnd()
_simpleTransition?.setOnSwipe(
OnSwipe().apply {
dragDirection = DRAG_END
touchAnchorId = R.id.default_button
touchAnchorSide = SIDE_START
onTouchUp = ON_UP_AUTOCOMPLETE_TO_START
setMaxAcceleration(500)
}
)
setTransition(_simpleTransition!!.id)
}
// Placeholder transition??
fun createPlaceholderTransition(motionScene: MotionScene): MotionScene.Transition? {
val startSetId = View.generateViewId()
val startSet = ConstraintSet()
startSet.clone(this)
val endSetId = View.generateViewId()
val endSet = ConstraintSet()
endSet.clone(this)
val transitionId = View.generateViewId()
return TransitionBuilder.buildTransition(
motionScene,
transitionId,
startSetId, startSet,
endSetId, endSet
)
}
/**
* Get px from dp
*/
private fun fromDp(context: Context, inDp: Int): Int {
val scale = context.resources.displayMetrics.density
return (inDp * scale).toInt()
}
}
Below is my adapter:
class SimpleMotionLayoutAdapter : RecyclerView.Adapter<SimpleMotionLayoutAdapter.ViewHolder>() {
val items = mutableListOf<Int>() // colors
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
fun setColor(color: Int) {
(view as SimpleMotionLayout).setSquareColor(color)
}
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = SimpleMotionLayout(parent.context)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setColor(items[position])
}
override fun getItemCount(): Int = items.size
companion object {
const val TYPE_NORMAL = 0
const val TYPE_EXCEPTIONAL = 1
}
}
Am i missing implementation?
Thank you

In general you need to cache and restore the state of the MotionLayout when it gets Recycled.
Right now in onBindViewHolder you only set the Color.
Remember RecyclerView keeps a only a screens worth (+ about 3) of ViewHolders and reuses them using onBindViewHolder
At minimum you need to set the Progress of the MotionLayout.
Due to differences in timing you may need to set the progress in an onPost

Related

Does the recyclerview/listview adapter have a method called once?

For a certain reason, I have to do one operation inside the adapter. In other words, this process should not be repeated when scrolling. The onCreate method in the activity works once when the activity is opened. Is there a method to do this inside the adapter?
UPDATE
My adapter class:
class EducationAdapter #Inject constructor(
var userManager: UserManager
) : ListAdapter<EducationModel, RecyclerView.ViewHolder>.
(EDUCATION_ITEM_DIFF_CALLBACK) {
companion object {
}
var callBack: EducationAdapterCallBack? = null
interface EducationAdapterCallBack {
fun onClickLayer(educationModel: EducationModel)
}
class EducationViewHolder(var binding: ItemEducationBinding) :
RecyclerView.ViewHolder(binding.root) {
fun setItem(item: EducationModel) {
binding.educationItem = item
}
}
#Nonnull
override fun onCreateViewHolder(
#Nonnull parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
val binding: ItemEducationBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_education, parent, false
)
return EducationAdapter.EducationViewHolder(binding)
}
override fun onBindViewHolder(#Nonnull holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
when (holder) {
is EducationAdapter.EducationViewHolder -> {
holder.setItem(item)
holder.binding.itemEducation.setOnClickListener {
callBack?.onClickLayer(item)
}
}
}
}
}
OncreateViewHolder and onBindViewHolder called when scrolling listview. But I want a single shot process is there any method that runs a single time?
UPDATE 2
I created a dummy operation (My original code was too long, I created summarized dummy operation). OnBind method should be like this:
override fun onBindViewHolder(#Nonnull holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
when (holder) {
is EducationAdapter.EducationViewHolder -> {
holder.setItem(item)
holder.binding.itemEducation.setOnClickListener {
callBack?.onClickLayer(item)
}
var isExpandClicked = false
holder.binding.btnExpandProject.setOnClickListener {
if (isExpandClicked) {
isExpandClicked = false
val context = holder.binding.llTaskFaaliyet.context
holder.binding.btnExpandProject.setImageDrawable(
ContextCompat.getDrawable(
context,
R.drawable.ic_arrow_down
)
)
val headerText = TextView(context)
headerText.apply {
text = subTask.title
textSize = 16f
setTextColor(ContextCompat.getColor(context, R.color.marine))
gravity = Gravity.START
typeface = ResourcesCompat.getFont(context, R.font.ubuntu_bold)
setPadding(48, 16, 4, 0)
setBackgroundColor(ContextCompat.getColor(context, R.color.white))
}
holder.binding.llTask.add(headerText)
}else{
isExpandClicked = true
val context = holder.binding.llTask.context
holder.binding.btnExpandProject.setImageDrawable(
ContextCompat.getDrawable(
context,
R.drawable.ic_close
)
)
holder.binding.llTask.removeAllViews()
}
}
}
}
}
I am trying to create those views below:
Expanded View:
Shrinked View
Views should be expanded at the first opening. It should be single time when the view is created.
If you put the function on bind method, it should be triggered whenever the item is shown. You can make a simple way, use boolean on adapter scope that will be changed to false after the expand function is triggered. Then use this boolean to check whether value is true, so it should trigger the expand function.

How to send RecyclerView data to a BottomSheetDialog

I am developing a Recyclerview that pressing a button should open a BottomSheetDialog. I can open BottomSheet but I can't pass data to it. I tried using an interface earlier but it didn't work.
class MyAdapter(private val listaItens: List<Itens>, private val context: Context,
private val fragmentManager: FragmentManager) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val POST_TXT = 0
private val POST_IMG = 1
//some code ...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
when (viewType) {
POST_TXT -> {
val item = LayoutInflater.from(parent.context).inflate(R.layout.text, parent, false)
return ViewHolderTexto(item)
}
POST_IMG -> {
val item = LayoutInflater.from(parent.context).inflate(R.layout.image, parent, false)
return ViewHolderImage(item)
}
//some code ...
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = listaItens[position]
val usuarioLogado = UsuarioFirebase.getDadosUsuarioLogado()
when (holder.itemViewType) {
POST_TXT -> {
val viewHolderTexto = holder as ViewHolderTexto
viewHolderTexto.setIsRecyclable(false)
//some code ...
}
POST_IMG -> {
val viewHolderImage = holder as ViewHolderImage
viewHolderImage.setIsRecyclable(false)
//some code ...
holder.imageComentarioPostagemImage.setOnClickListener {
val comentariosBottomSheet = ComentariosBottomSheet()//open bottom sheet
comentariosBottomSheet.show(fragmentManager, comentariosBottomSheet.tag)
}
//some code ...
}
}
}
How to send RecyclerView data to a BottomSheetDialog?
Why don't you simply pass the data by constructor param of the Bottom sheet
val comentariosBottomSheet = ComentariosBottomSheet(data)//open bottom sheet
comentariosBottomSheet.show(fragmentManager, comentariosBottomSheet.tag)
How about his approach:
function/interface passed into the adapter
Note function is passed as last parameter
class MyAdapter( .... private val onClickCallBack:(item :Itens) ... {
....
}
Register clicklistener on the holder
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int){
val item = listaItens[position]
val usuarioLogado = UsuarioFirebase.getDadosUsuarioLogado()
....
holder.itemView.setOnClickListener{
onClickCallBack(item)
}
Add callback to the adapter where you want to show the bottom (sheet ex Activity)
As noted above the function was passed as last parameter to the Aconstructor so we can call it after like this:
val myAdapter:MyAdapter = MyAdapter(){
//ShowBottomScree
// 'it' is refenced as the passed data from adapter.
}

(Kotlin) Position of RecyclerView returns -1 when trying to click on an item

I'm new to Android development (and Kotlin).
I'm trying to implement a RecyclerView (which works fine) and when I click on a specific row it opens a new activity (Intent).
However, whenever I've press/click on one of the rows, I'm only able to get the value "-1" returned.
I've tried a number of different approaches (you should see the number of tabs in my browser).
This seems like it should be a fairly straightforward occurrence for something as common as a RecyclerView, but for whatever reason I'm unable to get it working.
Here is my RecyclerView Adapter file:
class PNHLePlayerAdapter (val players : ArrayList<PNHLePlayer>, val context: Context) : RecyclerView.Adapter<ViewHolder>() {
var onItemClick: ((Int)->Unit) = {}
// Gets the number of items in the list
override fun getItemCount(): Int {
return players.size
}
// Inflates the item views
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(context).inflate(
R.layout.pnhle_list_item,
parent,
false
)
val viewHolder = ViewHolder(itemView)
itemView.setOnClickListener {
onItemClick(viewHolder.adapterPosition)
}
return ViewHolder(itemView)
}
// Binds each item in the ArrayList to a view
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.tvPlayerName?.text = players[position].Name
holder.tvPlayerRank?.text = position.toString()
holder.tvPNHLe?.text = players[position].PNHLe.toString()
holder.tvTeam?.text = players[position].Team
holder.ivLeague?.setImageResource(leagueImageID)
}
}
class ViewHolder (view: View) : RecyclerView.ViewHolder(view) {
val linLayout = view.hor1LinearLayout
val ivTeam = view.teamImageView
val tvPlayerName = view.playerNameTextView
val tvPlayerRank = view.rankNumTextView
val tvPNHLe = view.pnhleTextView
val tvTeam = view.teamTextView
val ivLeague = view.leagueImageView
}
As you can see, there is a class property "onItemClick" which uses a lambda as the click callback.
I setOnClickListener in the onCreateViewHolder method after the view is inflated.
Next, in my Activity I add the list to my Adapter and set the call back.
However, every time I 'Toast' the position it is displayed as '-1'.
val adapter = PNHLePlayerAdapter(list, this)
adapter.onItemClick = { position ->
Toast.makeText(this, position.toString(),Toast.LENGTH_SHORT).show()
var intent = Intent(this, PlayerCardActivity::class.java)
//startActivity(intent)
}
rv_player_list.adapter = adapter
Perhaps I'm not thinking about this properly, but shouldn't the position represent the row number of the item out of the RecyclerView???
Ideally, I need to use the position so that I can obtain the correct item from the 'list' (ArrayList) so that I can pass information to my next Activity using the Intent
I found the issue.
Change this line in onCreateViewHolder:
return ViewHolder(itemView)
to this one:
return viewHolder
I would reorganize the adapter like this:
class PNHLePlayerAdapter : androidx.recyclerview.widget.RecyclerView.Adapter<Adapter.ViewHolder>() {
interface AdapterListener {
fun onItemSelected(position: Int?)
}
var players: List<Player> = listOf()
set(value) {
field = value
this.notifyDataSetChanged()
}
var listener: AdapterListener? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_car_selector, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(position)
}
override fun getItemCount(): Int {
return brands.size
}
inner class ViewHolder(view: View): androidx.recyclerview.widget.RecyclerView.ViewHolder(view) {
private var position: Int? = null
private val baseView: LinearLayout? = view.findViewById(R.id.baseView) as LinearLayout?
...
init {
baseView?.setOnClickListener {
listener?.onManufacturerSelected(position)
}
}
fun bind(position: Int) {
this.position = position
...
}
}
}
And from your activity/fragment set the listener as adapter.listener = this, and implement the onItemSelected(position: Int?)
override fun onItemSelected(position: Int?) {
...
}

Can't assign new value to a simple Singleton using an onClickeListener for a RecyclerView

I am trying to build a Slack-like chat app following a tutorial in a course I am taking online.
In the tutorial the instructor is using a ListView and the OnItemClickListener method, but I am trying to do it with recycler view, and I am stuck with the onClickListener in the adapter.
I have tried to find answers in other questions but couldn't find one that solves my problem. The closest ones were this and this
My two problems are:
1. The app's main activity has on the top of the screen a title that states what channel is currently active. I have created a singleton that holds the "current channel" and the title's text is being pulled from that singleton.
I am having a hard time changing the value of that singleton on click.
The main activity also has all the channels in a list view in a drawer.
I am trying to close the drawer when a channel is clicked but that isn't happening either.
This is my current adapter:
class ChannelsAdapter(val context: Context, val channels: ArrayList<Channel>) :
RecyclerView.Adapter<ChannelsAdapter.Holder>() {
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val singleChannel = itemView.findViewById<TextView>(R.id.single_channel)
val mainLayout = LayoutInflater.from(context).inflate(R.layout.activity_main, null)
fun bindText(textVar: String, context: Context) {
singleChannel.text = textVar
}
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bindText(channels[position].toString(), context)
holder.itemView.setOnClickListener {
ChannelName.activeChannel = channels[position]
holder.mainLayout.drawer_layout.closeDrawer(GravityCompat.START)
}
}
override fun getItemCount(): Int {
return channels.count()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelsAdapter.Holder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.channel_list_layout, parent, false)
return Holder(view)
}
}
This is the singleton
object ChannelName {
var activeChannel : Channel? = null
}
You can rewrite the setter for activeChanell variable and call a listener that has been added before to notify your Activity:
object ChannelName {
private val listeners = ArrayList<(Channel?) -> Unit>()
fun addChannelNameChangedListener(listener: (Channel?) -> Unit) {
listeners.add(listener)
}
fun removeChannelNameChangedListener(listener: (Channel?) -> Unit) {
listeners.remove(listener)
}
var activeChannel: Channel? = null
set(value) {
field = value
listeners.forEach { it.invoke(value) }
}
}
And inside the Activity add a listener like this:
ChannelName.addChannelNameChangedListener {
// Do your operation
}
The alternative solution is to use Observable utils like LiveData, so you shouldn't worry about the Android life cycle any more:
object ChannelName {
val activeChannel: MutableLiveData<ChannelName> = MutableLiveData()
}
To change the value inside your adapter simply call:
ChannelName.activeChannel.value = channels[position]
And inside your activity Observe to the variable by calling:
ChannelName.activeChannel.observe(this, Observer {
// Do your operation
})
class ChannelsAdapter(val context: Context, val channels: ArrayList<Channel>) :
RecyclerView.Adapter<ChannelsAdapter.Holder>() {
private var itemClickListener: OnItemClickListener? = null
fun setItemClickListener(itemClickListener: OnItemClickListener) {
this.itemClickListener = itemClickListener
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
val singleChannel = itemView.findViewById<TextView>(R.id.single_channel)
val mainLayout = LayoutInflater.from(context).inflate(R.layout.activity_main, null)
fun bindText(textVar: String, context: Context) {
singleChannel.text = textVar
}
override fun onClick(v: View?) {
val position = adapterPosition
itemClickListener?.let {
if (position != RecyclerView.NO_POSITION) {
it.onItemClick(position)
}
}
}
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bindText(channels[position].toString(), context)
}
override fun getItemCount(): Int {
return channels.count()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelsAdapter.Holder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.channel_list_layout, parent, false)
return Holder(view)
}
}
This way you can setItemClickListener to the adapter in your activity and get callback from your recyclerView.
You should not set listener in onBind() method since it will be called more than your items' count.

RecycleView list items not appearing

I've been trying to make RecyclerView work in my app where the list items are ImageViews, and the images get downloaded and put inside (asynchronously) in the onBindViewHolder method. I'm not facing any errors in my code, but for some reason
only the list items which will be visible (even partially) to the user
when the activity loads, have images loaded into them.
Though I can't see the images, I observed that the height and width of these items have been allocated correctly. And since the images get downloaded first, and then the ImageView's dimensions are determined I figure that the problem has got something to do with RecyclerView itself? If someone can shed some light on this, it would be great. Thanks.
I would also like to add, that if the Activity is paused and then resumed (by clicking on the "square" navigation button and then resuming it), the images of all the list items load correctly.
Pic #1
Pic #2
Here's my code:
onCreate method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
recyclerView {
id = ViewID.ID_LIST
}
val imgList = ArrayList<ImageView>()
imgList.add(ImageView(ctx))
imgList.add(ImageView(ctx))
imgList.add(ImageView(ctx))
imgList.add(ImageView(ctx))
val lv = findViewById(ViewID.ID_LIST) as RecyclerView
lv.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
lv.adapter = ImageRecyclerAdapter(ctx, imgList)
}
The RecyclerView.Adapter class:
private class ImageRecyclerAdapter(val context: Context, val imageList: ArrayList<ImageView>) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onViewRecycled(holder: RecyclerView.ViewHolder?) {
super.onViewRecycled(holder)
if (holder != null) {
val v = holder.itemView as ImageView
v.setImageBitmap(null)
}
}
override fun onBindViewHolder(p: RecyclerView.ViewHolder, position: Int) {
val v = p.itemView as ImageView
Ion.with(v)
.load("https://pbs.twimg.com/profile_images/616076655547682816/6gMRtQyY.jpg")
.setCallback({ exception, t ->
if (t != null) {
val dm = Point()
context.windowManager.defaultDisplay.getRealSize(dm)
val w = t.maxWidth
val h = t.maxHeight
val params = t.layoutParams
if (params != null) {
params.width = dm.x
params.height = (dm.x * (h.toDouble() / w.toDouble())).toInt()
t.layoutParams = params
t.requestLayout()
}
}
})
}
override fun getItemCount(): Int {
return imageList.size
}
override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder {
val v = ImageView(context)
return object : RecyclerView.ViewHolder(v) {}
}
}
It worked after I made the binding of data into a Synchronous request, and I moved the ImageView manipulation (changing LayoutParams) into the onViewAttachedToWindow overridden method of my adapter.
onViewAttachedToWindow:
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder?) {
super.onViewAttachedToWindow(holder)
val t = holder?.itemView as ImageView
val dm = Point()
context.windowManager.defaultDisplay.getRealSize(dm)
val w = t.maxWidth
val h = t.maxHeight
val params = t.layoutParams
if (params != null) {
params.width = dm.x
params.height = (dm.x * (h.toDouble() / w.toDouble())).toInt()
t.layoutParams = params
t.requestLayout()
}
}
onBindViewHolder:
override fun onBindViewHolder(p: RecyclerView.ViewHolder, position: Int) {
val v = p.itemView as ImageView
Ion.with(v)
.load(imageList[position].toString())
.tryGet()
}

Categories

Resources