I want to create a list of views that align horizontally using recyclerview. I want to be split the width of the recyclerview equally so that all those views can fit inside a single line. view count is dynamic
Here is a simple final result, I created this using HTML and CSS.
Can I achieve the same in android using recyclerview?
Following is my recyclerview adapter, holder.root is the container for every view. I was thinking about getting the recyclerview width and divide that by number of views and setting that value as width for every view.
class ThreadsAdapter(
private val threads: ArrayList<ThreadInfo>
): RecyclerView.Adapter<ThreadsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_progress_thread, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
return threads.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.root.layoutParams.width = 40 // just testing
}
class ViewHolder(v: View): RecyclerView.ViewHolder(v) {
val progressBar: ProgressBar = v.findViewById(R.id.progressBar)
var root: ConstraintLayout = v.findViewById(R.id.root)
}
}
Eventually, I found this library on Github, allow me to use Flexbox layout in android
https://github.com/google/flexbox-layout
Related
This is how the current implementation when I scroll fast.
With in second the whole items in the recycler view are showed till the end.. What I am trying to achieve is something like the one shown below (live demo: recycler views in play store),
even if we scroll fast it shows 1 or 2 items so it feels natural. How can I make the recycler view behave this way?
The code:
Adapter
class CustomAdapter(private val mList: List<ItemsViewModel>) : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.card_view_design, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
}
override fun getItemCount(): Int {
return mList.size
}
class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) {
val imageView: ImageView = itemView.findViewById(R.id.imageview)
val textView: TextView = itemView.findViewById(R.id.textView)
}
}
Implementation
val recyclerview = findViewById<RecyclerView>(R.id.recyclerview)
recyclerview.layoutManager = LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false)
val data = ArrayList<ItemsViewModel>()
val adapter = CustomAdapter(data)
recyclerview.adapter = adapter
When a RecyclerView is scrolled horizontally, the scrollHorizontallyBy() is called several times with dx values that can make the scrolling behave smooth and real according to the fling (push) value of the finger.
The same for vertical RecyclerView, scrollVerticallyBy() gets called with different dy values.
So, you can manipulate this dx value to have the desired behavior; in your case you need to get smaller values of dx.
You'd think of a fixed dx value, but that won't be that natural; instead you can divide it by a fixed amount to scale down the same dx values.
Here I'm using an arbitrary value that you can manipulate it as you need.
So, what you need is to use a custom LinearLayoutManager & override scrollHorizontallyBy() as you use a horizontal recyclerView:
class MyLinearLayoutManager(context: Context?, orientation: Int, reverseLayout: Boolean) :
LinearLayoutManager(context, orientation, reverseLayout) {
override fun scrollHorizontallyBy(
dx: Int,
recycler: RecyclerView.Recycler?,
state: RecyclerView.State?
): Int {
var newDx = (dx / 1.2).toInt() // 1.2 is an arbitrary value
if (newDx == 0 && dx != 0) { // To have no 0 values (Optionally)
newDx = 1
}
return super.scrollHorizontallyBy(newDx, recycler, state)
}
}
I have differents screens that use recyclerview but they have a very similar item layout. The item layout is basically a textview and an image. The image is the same and never changes and the textview style is the same but the text changes because it comes from the api.
I started using the same item layout for each recyclerview because it was very similar but i don't know how to make it work and if it would take too much time
Below is the simplest way to change what to display. You can pass a boolean in the constructor of the adapter from the parent classes.
For example, If it is true, show some data. If it is false you can show some other data.
class RecyclerAdapter(var displayUserData: Boolean) : RecyclerView.Adapter<RecyclerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {...}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = values[position]
if(displayUserData){
holder.textView.text = item.name
}else{
holder.textView .text = item.otherStuff
}
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {...}
}
Then set the adapter with true or false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val adapter = RecyclerAdapter(true)
}
I am building an Android app with Kotlin and decided to replace the calls to findViewById and use binding. It all works fine but specifically, when I change an Adapter for a RecyclerView it breaks the item layout.
Original code with findViewById:
class WeightListAdapter(val weights: List<WeightWithPictures>, val onWeightItemClickListener: OnWeightItemClickListener) : RecyclerView.Adapter<WeightListAdapter.WeightHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.list_item_weight, parent, false)
return WeightHolder(view)
}
override fun onBindViewHolder(holder: WeightListAdapter.WeightHolder, position: Int) {
val weightWithPictures = weights[position]
holder.bind(weightWithPictures)
}
override fun getItemCount() = weights.size
inner class WeightHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private lateinit var weight: Weight
private val weightValueView: TextView = this.itemView.findViewById(R.id.weightValue)
private val weightDateView: TextView = this.itemView.findViewById(R.id.weightDate)
private val weightImageView: ImageView = this.itemView.findViewById(R.id.weightImage) as ImageView
And this is the layout:
But then whenever I use binding:
class WeightListAdapter(val weights: List<WeightWithPictures>, val onWeightItemClickListener: OnWeightItemClickListener) : RecyclerView.Adapter<WeightListAdapter.WeightHolder>() {
private var _binding: ListItemWeightBinding? = null
private val binding get() = _binding!!
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
_binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context))
val view = binding.root
return WeightHolder(view)
}
override fun onBindViewHolder(holder: WeightListAdapter.WeightHolder, position: Int) {
val weightWithPictures = weights[position]
holder.bind(weightWithPictures)
}
override fun getItemCount() = weights.size
inner class WeightHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private lateinit var weight: Weight
private val weightValueView: TextView = binding.weightValue
private val weightDateView: TextView = binding.weightDate
private val weightImageView: ImageView = binding.weightImage
The layout breaks:
Any ideas about what am I doing wrong here? Is it a bug?
P.S - For now, I am just adding the annotation to ignore bindings as documented here for the item view but I would really like to understand what's wrong.
Your binding needs to be inflated in the context of its parent so its root view's layout params will take effect:
binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context), parent, false)
I think it will also give you problems that you are creating a binding property for the Adapter if you try to use it long term. Each ViewHolder holds a distinct view with a distinct binding instance. It's working now because you use it only for the ViewHolder instantiation immediately after setting each instance. But if that's all your intent is, you should just pass the binding to the constructor of your ViewHolder and omit the adapter's property.
By the way, this is the sort of pattern I use for ViewHolders. Less code. Note, it doesn't have to be an inner class.
class WeightHolder(binding: ListItemWeightBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
fun bind(item: WeightWithPictures) {
with (binding) {
// set data for views here
}
}
}
I agree with #Tenfour04, using the same instance of binding is wrong but I believe the root cause of your issue is with the binding logic. with binding, the data is bound to bind with the view but not immediately. So your view gets inflated but since the binding happens at a later stage, scheduled to happen in near future, the item_view width is shrunk.
So try the following,
// oncreate view logic
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WeightListAdapter.WeightHolder {
val binding = ListItemWeightBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return WeightHolder(binding)
}
// onBindViewHolder logic remains the same
// this remains same as suggested by #Tenfour04 but a change in the bind function
class WeightHolder(binding: ListItemWeightBinding) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
fun bind(item: WeightWithPictures) {
with (binding) {
// set data for views using databindig
customVariable = item
executePendingBindings() // this is important
}
}
}
// define the customvariable in your `item_list_view.xml`
<variable
name="customVariable"
type="packagename.WeightWithPictures" />
executePendingBindings() is the way we force the binding to happen right away and not to schedule it later
Edit:
This answer is from Databinding perspective and not ViewBinding
I want to make sure that in a recycler view (horizontal orientation and LinearLayoutManager set) that if I have 2 items to display they both get 50% of the parent layout. So both fit in the screen.
I have tried 2 approaches but neither works properly.
Approach 1:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(R.layout.horizontal_list_item_view, parent, false)
itemView.post(
object : Runnable {
override fun run() {
itemView.layoutParams.width = (parent.width * 0.5).toInt()
}
}
)
return ViewHolder(itemView)
}
This does not work unless I first scroll and then only the first item has its width adapted.
Approach 2:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomAdapter.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val itemView = inflater.inflate(R.layout.horizontal_list_item_view, parent, false)
itemView.layoutParams.width = getScreenWidthFromUtils(context).x_width - MAGIC_NUMBER_WHICH_TAKES_PADDING_INTO_ACCOUNT)* 0.5).toInt()
return ViewHolder(itemView)
}
Approach 2 kind of works but obviously it can't be the solution as I doubt it has to be this complicated to achieve this.
What is the proper way to do this?
The GridLayoutManager will take care of giving your item the optimal size and space for the number of column you specify. In your case you want to have 2 items per row so your code should look like :
val gridLayoutManager = GridLayoutManager(context, 2, GridLayoutManager.HORIZONTAL, false)
The main recyclerview which is vertical recyclerview shows all the deals in the layout
But what I want to achieve is :
after the first 10 deals, I want to display a (horizontal recyclerview) of 5 recommended deal
after the next 10 deals, I want to display a (horizontal recyclerview) of 5 recommended deal
after the next 10 deals, I want to display a (horizontal recyclerview) of 5 recommended deal and so on…
I have attached the image of how i want the design to look like.
I have no clue on how to implement this. But I have both the recyclerview working separately. Just not sure how to combine them. I'm not even sure to code in the adapters or the mainactivity. I have attached the image of the horizontal recyclerview adapter and vertical recyclerview adapter. Please help me by showing me how to implement this feature. Thanks
class MyAdapter(val list: List<String?>): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val TYPE_ITEM = 0
private val TYPE_HORIZONTAL_ITEM = 1
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if(viewType == TYPE_ITEM) {
ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false))
}else HorizontalViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.horizontal_layout, parent, false))
}
override fun getItemCount(): Int {
return list.size
}
override fun getItemViewType(position: Int): Int {
return if(list[position]!=null) TYPE_ITEM else TYPE_HORIZONTAL_ITEM
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if(holder is ViewHolder){
holder.bind(list[position]?:"")
// manage your items
}else if(holder is HorizontalViewHolder)
holder.bind()
// manage your horizontal recyclerView
}
class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
fun bind(item: String){
}
}
class HorizontalViewHolder(itemView: View): RecyclerView.ViewHolder(itemView){
fun bind(){
}
}
You have to create Adapter which works with different viewTypes. As an example I've made list of Strings and every 10th element is null which is indicate that it's horizontal recyclerView. I believe that in your case instead of null it should be model for handling horizontal recyclerView. E.g list of 20 items should have size of 22 because there are two horizontal recyclerViews.
for (i in 1 until 25){
if (i % 10 == 0)
list.add(null)
else list.add("Hello")
}
recyclerView.adapter = MyAdapter(list)
enter image description here
I've attached final version of my result and that colored fields are for your RecyclerView.
I suggest you to avoid using inner RecyclerView and use NestedScrollView instead and add items through iteration.