The app contains two buttons, one for ListView and other for GridView, so I created to ItemViews and one adapter. I have created method, to switch between ListView and GridView. Every thing is working fine, except the GridView, which seems like it is jammed. List View is scrolling vertically but only upto 6 items, the list contains 9. I want the GridView to scroll in same fashion as List View scrolls until end. I have spent 2 simultaneous hours for searching solution, but none has worked. I have tried, ScrollView, NestedScrollView, but still unable to solve.
Grid View = RecyclerView with GridLayoutManager
Here is screenshot:
This one works fine upto 6 item and hide 3 out of 9
This one don't scroll (only GridView)
Here is Code:
activity_main.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context=".MainActivity">
<FrameLayout
android:id="#+id/frameLayout_imageContainer"
android:layout_width="match_parent"
android:layout_height="180dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="#+id/iv_house"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:srcCompat="#drawable/house" />
</FrameLayout>
<LinearLayout
android:id="#+id/linearLayout_btnContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:gravity="end"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="#id/frameLayout_imageContainer">
<ImageButton
android:id="#+id/btnListView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#drawable/btn_gradient_purple"
android:padding="8dp"
android:tint="#color/colorWhite"
app:srcCompat="#drawable/ic_list" />
<ImageButton
android:id="#+id/btnGridView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:background="#drawable/btn_gradient_purple"
android:padding="8dp"
android:tint="#color/colorWhite"
app:srcCompat="#drawable/ic_grid" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_room"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
app:layout_constraintTop_toBottomOf="#+id/linearLayout_btnContainer">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity.kt
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
private lateinit var mRoomList: ArrayList<Room>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val roomTypeBedroom = RoomType("Bedroom", R.drawable.ic_bedroom)
val roomTypeKitchen = RoomType("Kitchen", R.drawable.ic_kitchen)
val roomTypeBathroom = RoomType("Bathroom", R.drawable.ic_bathroom)
val roomTypeLiving = RoomType("Living", R.drawable.ic_living)
val roomTypeDining = RoomType("Dining", R.drawable.ic_dinning)
val roomTypeEmpty = RoomType("Empty", R.drawable.ic_empty)
mRoomList = arrayListOf(
Room("Bedroom 1", R.drawable.bedroom_1, 1, roomTypeBedroom),
Room("Bedroom 2", R.drawable.bedroom_2, 2, roomTypeBedroom),
Room("Bedroom 3", R.drawable.bedroom_3, 3, roomTypeBedroom),
Room("Kitchen", R.drawable.kitchen, 4, roomTypeKitchen),
Room("Bathroom", R.drawable.bathroom, 5, roomTypeBathroom),
Room("Living Room", R.drawable.living, 6, roomTypeLiving),
Room("Dining Room", R.drawable.sitting_area, 7, roomTypeDining),
Room("Empty Room 1", R.drawable.empty_1, 8, roomTypeEmpty),
Room("Empty Room 2", R.drawable.empty_2, 9, roomTypeEmpty)
)
showRoomsAs(ViewType.GRID)
btnGridView.setOnClickListener {
showRoomsAs(ViewType.GRID)
}
btnListView.setOnClickListener {
showRoomsAs(ViewType.LIST)
}
}
private fun showRoomsAs(viewType: ViewType) {
val roomAdapter = RoomAdapter(
this,
mRoomList,
viewType,
frameLayout_imageContainer
)
if (viewType == ViewType.GRID) {
rv_room.layoutManager = GridLayoutManager(this, 3)
btnGridView.visibility = View.GONE
btnListView.visibility = View.VISIBLE
} else {
rv_room.layoutManager = LinearLayoutManager(this)
btnGridView.visibility = View.VISIBLE
btnListView.visibility = View.GONE
}
rv_room.adapter = roomAdapter
}
}
RoomAdapter.k
import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class RoomAdapter(
private val context: Context,
private val rooms: ArrayList<Room>,
private val type: ViewType,
private val imageContainer: FrameLayout)
: RecyclerView.Adapter<RoomAdapter.RoomViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomViewHolder {
val inflater = LayoutInflater.from(parent.context)
var view: View? = null
if (type == ViewType.GRID)
view = inflater.inflate(R.layout.rv_item_grid, parent, false)
else if (type == ViewType.LIST)
view = inflater.inflate(R.layout.rv_item_list, parent, false)
return RoomViewHolder(view!!)
}
override fun getItemCount(): Int {
return rooms.size
}
override fun onBindViewHolder(holder: RoomViewHolder, position: Int) {
val room = rooms[position]
holder.icon.setBackgroundResource(room.type.icon)
holder.name.text = room.name
holder.type.text = room.type.type
holder.status.text = room.isRoomOnOrOff()
}
inner class RoomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
override fun onClick(p0: View?) {
val room = rooms[adapterPosition]
if (room.associatedImageView == null) {
val imageView = ImageView(context)
imageView.layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
imageView.setImageResource(room.image)
room.associatedImageView = imageView
imageContainer.addView(imageView)
}
if (room.isOn) {
room.associatedImageView?.visibility = View.GONE
room.isOn = false
} else {
room.associatedImageView?.visibility = View.VISIBLE
room.isOn = true
}
notifyDataSetChanged()
}
val icon: ImageView
val name: TextView
val type: TextView
val status: TextView
init {
itemView.setOnClickListener(this)
icon = itemView.findViewById(R.id.rv_item_icon) as ImageView
name = itemView.findViewById(R.id.rv_room_name) as TextView
type = itemView.findViewById(R.id.rv_room_type) as TextView
status = itemView.findViewById(R.id.rv_status) as TextView
}
}
}
any kind of quick help will be appreciated
ConstraintLayout can be tricky sometimes. Your recyclerView might be sitting inches away from app's view port. You can try the below attributes on your recyclerView
Option 1 : use of large padding
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_room"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="250dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom ="20dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent">
</androidx.recyclerview.widget.RecyclerView>
Option 2: Use of app:layout_constraintBottom_toBottomOf
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/rv_room"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="250dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom ="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="#+id/linearLayout_btnContainer">
</androidx.recyclerview.widget.RecyclerView>
Adding app:layout_constraintBottom_toBottomOf="parent" in second option forces the bottom of the recyclerView stick to the bottom of constraintLayout and not overflow
Related
I have a block of text that extends past both the display width and height. I want to scroll both vertically and horizontally. I am using a TextView and set both scrollbars. I also tried extending the size of the TextView past the parent. This correctly displays the String as I was hoping except the view does not scroll.
I have a simple app with two tabs. I change the text report based on tab selection which is very simple. I would like to make this work before complicating the code with fragments and pageviews
This is my main_activity.xml file...
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:tabGravity="fill"
app:tabMode="fixed" />
<TextView
android:id="#+id/report_view"
android:layout_width="1000dp"
android:layout_height="1000dp"
android:lineSpacingExtra="1sp"
android:scrollbars="horizontal|vertical"
android:typeface="monospace"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/tabs" />
</androidx.constraintlayout.widget.ConstraintLayout>
And this is my MainActivity.kt file...
package com.example.tabtest
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
class MainActivity : AppCompatActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val tabLayout: TabLayout = findViewById(R.id.tabs)
tabLayout.addTab(tabLayout.newTab().setText("Report 1"))
tabLayout.addTab(tabLayout.newTab().setText("Report 2"))
// report 1
var report1 = "Report 1"
for (index in 1..5)
{
report1 = "$report1\nline $index"
}
report1 = "$report1\nvery long line like this one: ################################################ "
// report 2
var report2 = "Monthly Report"
for (index in 1..25)
{
report2 = "$report2\nline $index"
}
val view: TextView = findViewById(R.id.report_view)
view.text = report1
tabLayout.addOnTabSelectedListener(object : OnTabSelectedListener
{
override fun onTabSelected(tab: TabLayout.Tab)
{
if (tabLayout.selectedTabPosition == 0)
{
view.text = report1
} else if (tabLayout.selectedTabPosition == 1)
{
view.text = report2
}
}
override fun onTabUnselected(tab: TabLayout.Tab)
{
}
override fun onTabReselected(tab: TabLayout.Tab)
{
}
})
}
}
This is my output...
Report 1 tab
Report 2 tab
I have got the Recyclerview working with findViewById but cant get it working with View bindings.
This Works
val tdList: RecyclerView = findViewById(R.id.td_list);
tdList.layoutManager = LinearLayoutManager(this)
tdList.setHasFixedSize(true);
tdList.adapter = reportAdapter
But this does not work
tdBinding.tdList.layoutManager = LinearLayoutManager(this)
tdBinding.tdList.setHasFixedSize(true);
tdBinding.tdList.adapter = reportAdapter
onCreateViewHolder or onBindViewHolder are never called and I get an error 'No adapter attached; skipping layout' so no data is shown in the RecyclerView.
I've been trying to get this working for a few days with View Bindings and finally found the code to get it working with findViewById and part of me thinks I should just be glad I got it working but would like to understand why (also as findViewById is quite expensive).
The full code is
DurationsReport.kt
package com.funkytwig.tasktimer
import android.database.Cursor
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.funkytwig.tasktimer.databinding.ActivityDurationsReportBinding
import com.funkytwig.tasktimer.databinding.TaskDurationsBinding
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
private const val TAG = "DurationsReportXX"
enum class SortColumns { NAME, DESCRIPTION, START_DATE, DURATION }
class DurationsReport : AppCompatActivity() {
private val reportAdapter by lazy { DurationsRVAdapter(this, null) }
var databaseCursor: Cursor? = null
var sortOrder = SortColumns.NAME
private val selection = "${DurationsContract.Columns.START_TIME} BETWEEN ? AND ?"
private var selectionArgs = arrayOf("0", "1559347199")
private lateinit var binding: ActivityDurationsReportBinding
private lateinit var tdBinding: TaskDurationsBinding
override fun onCreate(savedInstanceState: Bundle?) {
val func = "onCreate"
Log.d(TAG, func)
super.onCreate(savedInstanceState)
binding = ActivityDurationsReportBinding.inflate(layoutInflater)
tdBinding = TaskDurationsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
Log.d(TAG, "$func: Setup adapter")
val tdList: RecyclerView = findViewById(R.id.td_list);
tdList.layoutManager = LinearLayoutManager(this)
tdList.setHasFixedSize(true);
tdList.adapter = reportAdapter
// This does not work
// tdBinding.tdList.layoutManager = LinearLayoutManager(this)
// tdBinding.tdList.setHasFixedSize(true);
// tdBinding.tdList.adapter = reportAdapter
loadData()
}
private fun loadData() {
val func = "loadData"
Log.d(TAG, func)
val order = when (sortOrder) {
SortColumns.NAME -> DurationsContract.Columns.NAME
SortColumns.DESCRIPTION -> DurationsContract.Columns.DESCRIPTION
SortColumns.START_DATE -> DurationsContract.Columns.START_DATE
SortColumns.DURATION -> DurationsContract.Columns.DURATION
}
Log.d(TAG, "order=$order")
GlobalScope.launch {
val cursor = application.contentResolver.query(
DurationsContract.CONTENT_URI, null, selection, selectionArgs, order
)
Log.d(TAG, "$func: cursor.count=${cursor?.count}")
databaseCursor = cursor
reportAdapter.swapCursor(cursor)?.close()
}
}
}
DurationsRVAdapter.kt
package com.funkytwig.tasktimer
import android.content.Context
import android.database.Cursor
import android.text.format.DateFormat
import android.util.Log
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.funkytwig.tasktimer.databinding.TaskDurationItemsBinding
import java.util.Locale
import java.lang.IllegalStateException
private const val TAG = "DurationsRVAdapterXX"
class DurationsRVAdapter(context: Context, private var cursor: Cursor?) :
RecyclerView.Adapter<DurationsRVAdapter.DurationsViewHolder>() {
inner class DurationsViewHolder(val bindings: TaskDurationItemsBinding) :
RecyclerView.ViewHolder(bindings.root)
private val dateFormat = DateFormat.getDateFormat(context)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DurationsViewHolder {
Log.d(TAG, "onCreateViewHolder")
val view =
TaskDurationItemsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return DurationsViewHolder(view)
}
override fun onBindViewHolder(holder: DurationsViewHolder, position: Int) {
val func = "onBindViewHolder"
Log.d(TAG, "$func: position = $position")
val cursor = cursor
if (cursor != null && cursor.count != 0) {
if (!cursor.moveToPosition(position)) {
throw IllegalStateException("Couldn't move cursor to position $position")
}
val name = cursor.getString(cursor.getColumnIndex(DurationsContract.Columns.NAME))
val description =
cursor.getString(cursor.getColumnIndex(DurationsContract.Columns.DESCRIPTION))
val startTime =
cursor.getLong(cursor.getColumnIndex(DurationsContract.Columns.START_TIME))
val totalDuration =
cursor.getLong(cursor.getColumnIndex(DurationsContract.Columns.DURATION))
val userDate =
dateFormat.format(startTime * 1000) // The database stores seconds, we need milliseconds
val totalTime = formatDuration(totalDuration)
holder.bindings.tdName.text = name
holder.bindings.tdDescription?.text = description
holder.bindings.tdStart.text = userDate
holder.bindings.tdDuration.text = totalTime
}
}
private fun formatDuration(duration: Long): String {
// convert duration Long to hours:mins:secs String (can be > 24 hours so cant use dateFormat)
val hours = duration / 3600
val remainder = duration - hours * 3600
val minutes = remainder / 60
val seconds = remainder % 60
return String.format(Locale.getDefault(), "%02d:%02d:%02d", hours, minutes, seconds)
}
override fun getItemCount(): Int {
val func = "getItemCount"
val count = cursor?.count ?: 0
Log.d(TAG, "$func: count=$count")
return count
}
fun swapCursor(newCursor: Cursor?): Cursor? {
val func = "swapCursor"
Log.d(TAG, func)
if (newCursor === cursor) return null
val numItems = itemCount
val oldCursor = cursor
cursor = newCursor
Log.d(TAG, "$func: cursor.count=${cursor?.count}")
Log.d(TAG, "$func newCursor.count=${newCursor?.count}, oldCursor.cont=${oldCursor?.count}")
if (newCursor != null) {
Log.d(TAG, "$func notify the observers about the new cursor")
// notify the observers about the new cursor
this.notifyDataSetChanged()
Log.d(TAG, "$func: notifyDataSetChanged")
} else {
Log.d(TAG, "$func Notify observer about lack of dataset")
// Notify observer about lack of dataset, all of it from 0 to newItems,
// i.e. whole range of records has gone
this.notifyItemRangeChanged(0, numItems)
Log.d(TAG, "$func: notifyItemRangeChanged(0, $numItems)")
}
return oldCursor
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
val func = "onAttachedToRecyclerView"
Log.d(TAG, "$func: ${recyclerView.adapter.toString()}")
super.onAttachedToRecyclerView(recyclerView)
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
val func = "onDetachedFromRecyclerView"
Log.d(TAG, "$func: ${recyclerView.adapter.toString()}")
super.onDetachedFromRecyclerView(recyclerView)
}
}
task_durations.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
app:layout_constraintHorizontal_chainStyle="spread">
<TextView
android:id="#+id/td_name_heading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:background="?attr/colorButtonNormal"
android:padding="4dp"
android:text="#string/td_text_name"
android:textAlignment="viewStart"
android:textStyle="bold"
app:layout_constraintHorizontal_weight="2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread" />
<TextView
android:id="#+id/td_start_heading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="?attr/colorButtonNormal"
android:padding="4dp"
android:text="#string/td_text_date"
android:textAlignment="viewStart"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="#+id/td_name_heading"
app:layout_constraintEnd_toStartOf="#+id/td_duration_heading"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="#+id/td_name_heading" />
<TextView
android:id="#+id/td_duration_heading"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="?attr/colorButtonNormal"
android:padding="4dp"
android:text="#string/td_text_duration"
android:textAlignment="viewStart"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="#+id/td_start_heading"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="#+id/td_start_heading" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/td_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/td_name_heading"
tools:listitem="#layout/task_duration_items" />
</androidx.constraintlayout.widget.ConstraintLayout>
task_duration_itesm.xml
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:showIn="#layout/task_durations">
<TextView
android:id="#+id/td_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:padding="4dp"
android:textAlignment="viewStart"
app:layout_constraintHorizontal_weight="2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread"
tools:text="#string/td_text_name" />
<TextView
android:id="#+id/td_start"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:padding="4dp"
android:textAlignment="viewStart"
app:layout_constraintBaseline_toBaselineOf="#+id/td_name"
app:layout_constraintEnd_toStartOf="#+id/td_duration"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="#+id/td_name"
tools:text="#string/td_text_date" />
<TextView
android:id="#+id/td_duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:padding="4dp"
android:textAlignment="viewStart"
app:layout_constraintBaseline_toBaselineOf="#+id/td_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="#+id/td_start"
tools:text="#string/td_text_duration" />
</androidx.constraintlayout.widget.ConstraintLayout>
If it works with findViewById, then your view is in your binding and not your tdBinding, because findViewById searches the layout that you set with setContentView.
If it didn't give a compiler error, you must have a view with the same ID in both layouts. I don't know why you have two layouts, since you're only using one of them.
View Binding is not better performing than findViewById unless you're using findViewById repeatedly to find the same view. I'm pretty sure View Binding uses findViewById internally. The purpose of View Binding is not better performance. It's for eliminating the need to manually sync up your code with your XML and risking runtime errors instead of compile-time errors.
OK, the answer was to just use one View Binding. The master XML layout has the other layout as an include. The trick is to give the include an ID then you can traverse the XML from both layouts in a single view binding.
<include
android:id="#+id/items"
layout="#layout/task_durations" />
The Kotlin code to to setup RecyclerView is
binding.items.tdList.layoutManager = LinearLayoutManager(this)
binding.items.tdList.setHasFixedSize(true)
binding.items.tdList.adapter = reportAdapter
I think you generaly only need a single View Binding where you are using include, but as I said to traverse them you need to give the include a ID.
I have a form which contains 2 fields and under those, few generated buttons in a RecyclerView. The RecyclerView has a GridLayout of 2 columns. My cells
I just noticed a bug which appears when I close the native keyboard (it has opened after I filled my two fields)
If I take the example of 3 buttons, I would have in my RecyclerView one row with 2 cells and a second row with one.
After closing my keyboard, the RecyclerView gets wrapped into one row of 2 cells and to access the second row, I have to scroll inside the RecyclerView.
I tried with a LinearLayout, same bug.
I tried few fixes found on StackOverFlow :
setting the keyboard to adjustPan in the Manifest
setting height to match_parent to my cells and layouts
None of those worked.
Here are some screens of the bugs:
Before opening the keyboard :
After closing the keyboard :
Any idea on how to fix this ?
Best regards
UPDATE 1 :
As asked, here are some code samples to reproduce the problem :
Adapter Setup :
binding.actionButtonsContainer.setLayoutColumnsCount(actionsList.size)
val adapter = IncidentActionButtonsListAdapter(actionsList, this)
binding.actionButtonsContainer.setAdapter(adapter)
binding.actionButtonsContainer.visibility = View.VISIBLE
My Adapter :
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
class IncidentActionButtonsListAdapter(
private val actionsList: List<IncidentAction>,
private val incidentActionButtonViewClickListener: IncidentActionButtonViewClickListener? = null
): RecyclerView.Adapter<IncidentActionButtonsListAdapter.IncidentActionButtonsListViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IncidentActionButtonsListViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.layout_incident_action_button_item, parent, false)
return IncidentActionButtonsListViewHolder(view)
}
override fun onBindViewHolder(holder: IncidentActionButtonsListViewHolder, position: Int) {
actionsList[position].let { action ->
holder.tvIncidentActionTitle.text = holder.tvIncidentActionTitle.context.getString(action.text)
holder.itemView.setOnClickListener {
val incidentActionResult = IncidentActionResult(
code = action.id
)
incidentActionButtonViewClickListener?.onIncidentActionButtonViewClicked(incidentActionResult)
}
}
}
inner class IncidentActionButtonsListViewHolder(view: View): RecyclerView.ViewHolder(view) {
val container: LinearLayout = view.findViewById(R.id.container)
val tvIncidentActionTitle: TextView = view.findViewById(R.id.tvIncidentActionTitle)
}
override fun getItemCount(): Int = actionsList.size
}
interface IncidentActionButtonViewClickListener {
fun onIncidentActionButtonViewClicked(incidentActionResult: IncidentActionResult)
}
class IncidentActionButtonsItemDecorator (private val padding: Int) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
)
{
super.getItemOffsets(outRect, view, parent, state)
outRect.top = padding
outRect.bottom = padding
outRect.left = padding
outRect.right = padding
}
}
The RecyclerView code :
class IncidentActionButtonView(
context: Context,
attrs: AttributeSet
): LinearLayout(context, attrs) {
var binding: LayoutIncidentActionListBinding = LayoutIncidentActionListBinding.inflate(LayoutInflater.from(context), this, true)
init {
val spacing = (context.resources.displayMetrics.density * 4).toInt() // converting dp to pixels
binding.list.addItemDecoration(IncidentActionButtonsItemDecorator(spacing)) // setting space between items in RecyclerView
}
fun setLayoutColumnsCount(numberOfActions: Int) {
var numberOfColumns = numberOfActions
val orientation = resources.configuration.orientation
// if phone is in landscape orientation we can accept up to 3 cols
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
if (numberOfColumns > 3) {
numberOfColumns = 3
}
} else { // else in portrait we can accept up to 2 cols
if (numberOfColumns > 2) {
numberOfColumns = 2
}
}
val layoutManager = GridLayoutManager(context, numberOfColumns)
binding.list.layoutManager = layoutManager
}
fun setAdapter(adapter: IncidentActionButtonsListAdapter) {
binding.list.adapter = adapter
binding.list.visibility = View.VISIBLE
}
}
The RecyclerView Layout :
<?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:id="#+id/actionButtonsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#drawable/incident_list_background"
app:layout_constraintTop_toBottomOf="#id/tvTitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
The cells layout :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/container"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:background="#drawable/incident_action_button_background"
android:orientation="horizontal"
android:paddingLeft="#dimen/padding_small"
android:paddingRight="#dimen/padding_small"
android:paddingTop="#dimen/padding_extra_small"
android:paddingBottom="#dimen/padding_extra_small">
<TextView
style="#style/Theme.PortailAchat.Title1"
android:id="#+id/tvIncidentActionTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#color/white"
android:text="#string/incident_await_new_delivery"
android:textAllCaps="true"
android:textAlignment="center"
android:layout_gravity="center"/>
</LinearLayout>
add this line of code inside the activity tag in manifest file:
android:windowSoftInputMode="adjustPan"
I am trying to create a recycler listview that houses integer numbers between 1 and 250.
The issue I am having is that when I scroll through the list, it only displays 1-9 and then randomly shows only single digits. Is it recycling the cached item values?
Here is how my adapter looks:
package com.work.me
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.counter_layout.view.*
class CounterAdapter : RecyclerView.Adapter<CounterAdapter.ViewHolder>() {
private val counterList = mutableListOf<Int>()
init {
for (x in 0..COUNTER_MAX) {
Log.d("JJJ", "x is $x")
counterList.add(x + COUNTER_OFFSET)
}
}
inner class ViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bindView(counterValue: String) {
Log.d("JJJ", "counterValue is $counterValue")
view.counterText.text = counterValue
Log.d("JJJ", " view.counterText is ${view.counterText.text}")
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.counter_layout, parent, false)
return ViewHolder(view)
}
override fun getItemCount(): Int {
Log.d("JJJ", "size is " + counterList.size)
return counterList.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindView(counterList[position + COUNTER_OFFSET].toString())
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
companion object {
const val COUNTER_MAX = 250
const val COUNTER_OFFSET = 1
}
}
I have placed logs as you can see and on the bindView function, the value passed is correct but never displayed on the actuial list ui widget.
Here is how i initiate the list widget with the adapter:
counterList.apply {
adapter = counterAdapter
// setHasFixedSize(true)
layoutManager = LinearLayoutManager(this#MainActivity)
}
Here is the counter layout of each item:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="#+id/counterText"
style="#style/TextAppearance.AppCompat.Headline" />
</androidx.constraintlayout.widget.ConstraintLayout>
Activty layout
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
......
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/counterList"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginTop="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="#id/counterButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
Thanks in advance
I think you have the old view reference in the holder.
Try to get rid of it and use itemView instead
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bindView(counterValue: String) {
Log.d("JJJ", "counterValue is $counterValue")
itemView.counterText.text = counterValue
Log.d("JJJ", " view.counterText is ${itemView.counterText.text}")
}
}
I have implemented a simple adapter but it is causing RecyclerView not to recycler views and calls onCreateViewHolder() for every list item when scrolled. This causes jank
whenever I scroll the list. Few points listed below are not related to excessive calls of onCreateViewHolder(), but I tried them to improve scroll performance and avoid jank. Things I have tried so far:
recyclerView.setHasFixedSize(true)
recyclerView.recycledViewPool.setMaxRecycledViews(1, 10) with recyclerView.setItemViewCacheSize(10)
recyclerView.setDrawingCacheEnabled(true) with recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH)
setting RecyclerView height to "match_parent"
Was previously using Kotlin's synthetic, now moved to Android's ViewBinding
Rewrite complex nested layouts to Constraint Layout
override onFailedToRecycleView() to see if it is called, but it was never called
Here is my adapter:
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.suppstore.R
import com.example.suppstore.common.Models.Brand
import com.example.suppstore.databinding.LiBrandBinding
import com.google.firebase.perf.metrics.AddTrace
class BrandsAdapter(list: ArrayList<Brand>, var listener: BrandClickListener?) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val VIEW_TYPE_LOADING = 0
private val VIEW_TYPE_NORMAL = 1
private var brandsList: ArrayList<Brand> = list
#AddTrace(name = "Brands - onCreateViewHolder", enabled = true)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == VIEW_TYPE_NORMAL) {
ViewHolder(
LiBrandBinding.inflate(
LayoutInflater.from(parent.context),
parent, false
)
)
} else {
LoaderHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.li_loader, parent, false)
)
}
}
#AddTrace(name = "Brands - onBindViewHolder", enabled = true)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is ViewHolder)
holder.setData(brandsList[position], listener!!)
}
class ViewHolder(itemView: LiBrandBinding) : RecyclerView.ViewHolder(itemView.root) {
private val binding: LiBrandBinding = itemView
#AddTrace(name = "Brands - ViewHolder-setData", enabled = true)
fun setData(brand: Brand, listener: BrandClickListener) {
binding.cardItem.setOnClickListener { listener.onItemClick(brand) }
binding.tvBrandName.text = brand.name
binding.tvCount.text = brand.count.toString() + " Products"
}
}
class LoaderHolder(itemView: View) : RecyclerView.ViewHolder(itemView.rootView) {
}
#AddTrace(name = "Brands - addLoader", enabled = true)
fun addLoader() {
brandsList.add(Brand())
notifyItemInserted(brandsList.size - 1)
}
#AddTrace(name = "Brands - setData", enabled = true)
fun setData(newList: ArrayList<Brand>) {
this.brandsList = newList
notifyDataSetChanged()
}
#AddTrace(name = "Brands - removeLoader", enabled = true)
fun removeLoader() {
if (brandsList.size == 0)
return
val pos = brandsList.size - 1
brandsList.removeAt(pos)
notifyItemRemoved(pos)
}
override fun getItemViewType(position: Int): Int {
return if (brandsList.get(position).count == -1) {
VIEW_TYPE_LOADING
} else
VIEW_TYPE_NORMAL
}
interface BrandClickListener {
fun onItemClick(brand: Brand)
}
override fun getItemCount(): Int {
return brandsList.size
}
}
Here is the list item (li_brand):
<?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:id="#+id/cardItem"
android:layout_width="match_parent"
android:layout_height="85dp"
android:background="#color/app_black">
<TextView
android:id="#+id/tvBrandName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:textColor="#color/app_yellow"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="#id/tvCount"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="#+id/tvCount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginTop="2dp"
android:textColor="#color/app_grey"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="#id/tvBrandName" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="20dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="15dp"
android:src="#drawable/ic_baseline_arrow_forward_ios_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="3dp"
android:background="#color/app_bg"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Here are related functions in Fragment
class BrandsFragment : Fragment() {
private val adapter = BrandsAdapter(ArrayList(), brandClickListener())
fun brandClickListener(): BrandsAdapter.BrandClickListener {
return object : BrandsAdapter.BrandClickListener {
override fun onItemClick(brand: Brand) {
activityViewModel?.setSelectedBrand(brand)
}
}
}
fun setupRecyclerView() {
val llManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
binding.recyclerView.layoutManager = llManager
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) { //check for scroll down
val visibleItemCount = llManager.childCount
val totalItemCount = llManager.itemCount
val firstVisibleItemPos = llManager.findFirstVisibleItemPosition()
if (loadWhenScrolled
&& visibleItemCount + firstVisibleItemPos >= totalItemCount
&& firstVisibleItemPos >= 0
) {
//ensures that last item was visible, so fetch next page
loadWhenScrolled = false
viewModel.nextPage()
}
}
}
})
binding.recyclerView.adapter = adapter
}
}
And here is the fragment 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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/app_black"
android:focusableInTouchMode="true"
android:orientation="vertical"
tools:context=".Brands.BrandsFragment">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="#drawable/bottom_line_yellow"
android:theme="#style/SearchViewTheme"
app:closeIcon="#drawable/ic_baseline_close_24"
app:iconifiedByDefault="false"
app:queryBackground="#android:color/transparent"
app:queryHint="Atleast 3 characters to search"
app:searchIcon="#drawable/ic_baseline_search_24" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Have you tried RecyclerView Diffutil class? Hope it will resolve smooth scrolling issue and overwhelm recreation of items.
https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil
"DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one."