I have made an app in Android Studio which imports data from a website...when I open the app the card views in which the data is to be imported are empty and get loaded after some time. I wanted to know if there is any way in which I can cover it up so that till the time it is being loaded my splash screen stays there. Here is a video showing the lag:
Card View loading time lag
Edit: Here is the movie Adapter:
class MoviesAdapter(
private var movies: MutableList<Movie>,
private val onMovieClick: (movie: Movie) -> Unit,
) : RecyclerView.Adapter<MoviesAdapter.MovieViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MovieViewHolder {
val view = LayoutInflater
.from(parent.context)
.inflate(R.layout.item_movie, parent, false)
return MovieViewHolder(view)
}
override fun getItemCount(): Int = movies.size
override fun onBindViewHolder(holder: MovieViewHolder, position: Int) {
holder.bind(movies[position])
}
fun appendMovies(movies: List<Movie>) {
this.movies.addAll(movies)
notifyItemRangeInserted(
this.movies.size,
movies.size - 1
)
}
inner class MovieViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val poster: ImageView = itemView.findViewById(R.id.item_movie_poster)
fun bind(movie: Movie) {
itemView.setOnClickListener { onMovieClick.invoke(movie) }
Glide.with(itemView)
.load("https://image.tmdb.org/t/p/w342${movie.posterPath}")
.transform(CenterCrop())
.into(poster)
}
}
}
My main_activity_xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/bg_gradient"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="16dp"
android:text="#string/discover"
android:textColor="#android:color/white"
android:textSize="45sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="#string/popular"
android:textColor="#android:color/white"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="#string/most_popular_movies" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/popular_movies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="#string/top_rated"
android:textColor="#android:color/white"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="#string/highest_rated_movies" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/top_rated_movies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="#string/upcoming"
android:textColor="#android:color/white"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="#string/stay_updated" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/upcoming_movies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="#string/now_playing"
android:textColor="#android:color/white"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="#string/now_in_Cinemas" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/now_playing_movies"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="10dp"
android:clipToPadding="false"
android:paddingStart="16dp"
android:paddingEnd="16dp" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
Api Key:
interface Api {
#GET("movie/popular")
fun getPopularMovies(
#Query("api_key") apiKey: String =
"eff7d87eb14cdf1fb273414692d3e3a8",
#Query("page") page: Int,
): Call<GetMoviesResponse>
}
Api Loader:
api.getPopularMovies(page = page)
.enqueue(object : Callback<GetMoviesResponse> {
override fun onResponse(
call: Call<GetMoviesResponse>,
response: Response<GetMoviesResponse>,
) {
if (response.isSuccessful) {
val responseBody = response.body()
if (responseBody != null) {
onSuccess.invoke(responseBody.movies)
} else {
onError.invoke()
}
} else {
onError.invoke()
}
}
override fun onFailure(call: Call<GetMoviesResponse>, t:
Throwable) {
onError.invoke()
}
})
Is there anything else that I should specify?
without code it's hard to answer the question, but you could always use a ViewSwitcher that displays a "Loading" state before the data for the cardviews are ready, but holds the space, so the UI won't jump, and then have the Card view displayed once the data is loaded.
Could be something like:
sealed class CardData {
object Loading: CardData()
class Info(val items: List<CardInfo>): CardData()
}
fun loadData() {
viewModel.cardData.observe { cardData ->
when(cardData) {
is Loading -> {
binding.switcher.displayedChild = 0
}
is Info -> {
binding.switcher.displayedChild = 1
binding.listOfCards.items = cardData.items
}
}
}
for your specific case:
// display the loading view before making the call
binding.switcher.displayedChild = 0
api.getPopularMovies(page = page)
.enqueue(object : Callback<GetMoviesResponse> {
override fun onResponse(
call: Call<GetMoviesResponse>,
response: Response<GetMoviesResponse>,
) {
if (response.isSuccessful) {
val responseBody = response.body()
if (responseBody != null) {
onSuccess.invoke(responseBody.movies)
} else {
onError.invoke()
}
} else {
onError.invoke()
}
}
override fun onFailure(call: Call<GetMoviesResponse>, t:
Throwable) {
onError.invoke()
}
})
// in your onSuccess()
private void onSuccess(List<Movies> movies) {
binding.switcher.displayedChild = 1
listAdapter.items = movies
listAdapter.notifyDatasetChanged()
}
// in your onError()
private void onError() {
binding.switcher.displayedChild = 2 // you can add a third child for errors
}
then the XML would be something like (missing attributes):
<ViewSwitcher
android:id="#+id/switcher">
<!-- child 0 - loading state -->
<ProgressBar
android:id="#+id/loader"
/>
<!-- child 1 - movies state -->
<RecyclerView
android:id="#+id/movies"
/>
<!-- child 2 - error state -->
<TextView
android:id="#id/error_text"
/>
</ViewSwitcher>
If you want to wait in the splash screen, you will need to make that request in the splash screen.
When you have a successful response, you then navigate and pass the data through args/intent extras, assuming this is a different fragment/activity.
Then, when you are setting creating the list adapter you just need to pass this list through the constructor and set wire everything up, like you probably already have.
Hope it helps, but let me know if this wasn't clear enough!
You can create a Splash screen view exactly like your splash screen in your main activity and show it first while loading all the data from the network in the background, once the data arrives, hide this splash screen view and show app's main screen.
You can instead use shimmer effect to show data loading in background.
Related
I'm new to Kotlin and Android development in general. I have created a custom adapter that extends BaseAdapter and it works as I want it to, but as soon as I was to add a click listener to the view that gets returned in the getView, it doesn't seem to work as expected.
I have looked everywhere for an answer. I have seen people talking about clickable, duplicateParentState, focusable,... But nothing seems to work
The click listener does react if I click on the left/right border of my layout RelativeLayout, but I want the entire RelativeLayout to react when I click.
Am I missing something in my XML? Or is there some kind of "hack" that works. I'm a little confused as to why my click listener is not working as expected
My TokenAdapter (extends BaseAdapter)
class TokenAdapter(private val ctx: Context) : BaseAdapter() {
private val tokenPersistence: TokenPersistence = TokenPersistence(ctx)
private val clipboardManager: ClipboardManager =
ctx.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
var seconds: Int
var percentage: Int = 100
var shouldGenerateToken: Boolean = true
init {
seconds = getSecondsUntilRefresh()
}
fun getSecondsUntilRefresh() : Int{
val secondsElapsedInMinute = LocalDateTime.now().second
return if(secondsElapsedInMinute < 30) 30 - secondsElapsedInMinute else 60 - secondsElapsedInMinute
}
override fun getCount(): Int {
return tokenPersistence.length()
}
override fun getItem(position: Int): Token? {
return tokenPersistence.get(position)
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val v: View = if (convertView == null) {
val inflater: LayoutInflater =
ctx.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.list_item, parent, false)
} else {
convertView
}
val t: Token = getItem(position)!!
val title: TextView = v.findViewById(R.id.list_item_title)
val code: TextView = v.findViewById(R.id.list_item_subtitle)
title.text = t.getLabel()
if (shouldGenerateToken) {
var generatedCode : String = t.generateCode()
generatedCode = generatedCode.substring(0, 3) + " " + generatedCode.substring(3, generatedCode.length)
code.text = generatedCode
shouldGenerateToken = false
}
val countdown: ProgressBar = v.findViewById(R.id.progress_circular)
val countdownText: TextView = v.findViewById(R.id.progress_circular_text)
countdownText.text = seconds.toString()
countdown.progress = percentage
v.setOnClickListener {
val copyText = code.text.split(' ').joinToString("")
println("===================")
println("Copied: $copyText")
val clip: ClipData = ClipData.newPlainText("2FA Code", copyText)
clipboardManager.setPrimaryClip(clip)
Toast.makeText(ctx, "Copied: $copyText to clipboard", Toast.LENGTH_LONG).show()
}
return v
}
}
The list_item XML layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/list_item_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/list_item_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="16sp"
android:textStyle="bold"
android:paddingHorizontal="8dp"
android:paddingTop="16dp"
android:paddingBottom="0dp"
android:clickable="false"
/>
<TextView
android:id="#+id/list_item_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_below="#id/list_item_title"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_toStartOf="#id/progress_circular_container"
android:layout_toLeftOf="#id/progress_circular_container"
android:paddingHorizontal="8dp"
android:paddingTop="0dp"
android:text="123456"
android:textColor="#color/primary"
android:textSize="32sp"
android:textStyle="bold"
android:clickable="false"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/progress_circular_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_below="#id/list_item_title"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:clickable="false">
<ProgressBar
android:id="#+id/progress_circular"
android:layout_width="48sp"
android:layout_height="48sp"
android:indeterminateOnly="false"
android:progressDrawable="#drawable/pb_circular_determinative"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:progress="100"/>
<TextView
android:id="#+id/progress_circular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#android:color/black"
app:layout_constraintBottom_toBottomOf="#+id/progress_circular"
app:layout_constraintEnd_toEndOf="#+id/progress_circular"
app:layout_constraintStart_toStartOf="#+id/progress_circular"
app:layout_constraintTop_toTopOf="#+id/progress_circular"
tools:text="30" />
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
So I managed to fix my problem.
I did 2 "major" things after which my problem was resolved:
I rewrote my list_item.xml making use of LinearLayout instead of RelativeLayout (although I doubt this did anything)
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="#+id/list_item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="16sp"
android:textStyle="bold"
android:paddingHorizontal="8dp"
android:paddingTop="16dp"
android:paddingBottom="0dp"
/>
<TextView
android:id="#+id/list_item_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp"
android:paddingTop="0dp"
android:textColor="#color/primary"
android:textSize="32sp"
android:textStyle="bold"
/>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center">
<ProgressBar
android:id="#+id/progress_circular"
android:layout_width="48sp"
android:layout_height="48sp"
android:indeterminateOnly="false"
android:progressDrawable="#drawable/pb_circular_determinative"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:progress="100"/>
<TextView
android:id="#+id/progress_circular_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#android:color/black"
app:layout_constraintBottom_toBottomOf="#+id/progress_circular"
app:layout_constraintEnd_toEndOf="#+id/progress_circular"
app:layout_constraintStart_toStartOf="#+id/progress_circular"
app:layout_constraintTop_toTopOf="#+id/progress_circular"
tools:text="30" />
</androidx.constraintlayout.widget.ConstraintLayout>
I moved my ClickListener from my Adapter getView() to my MainActivity with an setOnItemClickListener on my ListView
val listview = findViewById<ListView>(R.id.tokenList)
listview.setOnItemClickListener { parent: AdapterView<*>, view: View, position: Int, id ->
val token = tokenAdapter.getItem(position)
if(token != null) {
val code = token.generateCode()
val clip: ClipData = ClipData.newPlainText("2FA Code", code)
clipboardManager.setPrimaryClip(clip)
Toast.makeText(this, "Copied: $code", Toast.LENGTH_LONG).show()
}
}
Using this method on my Listview I also didn't have to use clickable, or any of those kinds of properties on my XML layouts. If someone knows what exactly made it work, please tell me. Personally I think it is the Listener being an 'OnItemClick' and it being moved to the MainActivity, although don't take my word for it as it could just be something else random that I did.
The above is what I am trying to achieve .
My recycle view item has two buttons . Both are hidden by default . On click of the item view should show the buttons for 1 sec. After it must hide . if I tap on either of these buttons , the timer should be reset for another 1 sec . Something similar to a debounce in rx java.
Need to do it with Kotlin coroutines .
I was able to achieve the above using kotlin coroutines jobs .
My layout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="150dp"
android:layout_height="250dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_marginHorizontal="5dp"
android:elevation="5dp"
android:layout_gravity="bottom"
>
<TextView
android:id="#+id/product_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="#id/guideline_horizontal_top"
android:text="x2"
android:fontFamily="#font/gotham"
android:textColor="#color/dark_blue"
android:textSize="20sp"/>
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline_horizontal_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent=".20"/>
<androidx.cardview.widget.CardView
android:id="#+id/part_card"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="#id/guideline_horizontal_top"
app:layout_constraintBottom_toBottomOf="parent"
app:cardElevation="5dp"
android:clickable="true"
android:foreground="?attr/selectableItemBackgroundBorderless"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<com.google.android.material.button.MaterialButton
android:id="#+id/plus_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="#color/transparent_dark_blue"
app:layout_constraintTop_toTopOf="parent"
android:insetTop="0dp"
app:cornerRadius="0dp"
android:text="ADD"
android:visibility="invisible"/>
<com.google.android.material.button.MaterialButton
android:id="#+id/minus_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="#color/transparent_dark_blue"
app:layout_constraintBottom_toBottomOf="parent"
android:insetBottom="0dp"
app:cornerRadius="0dp"
android:text="REDUCE"
android:visibility="invisible"/>
<TextView
android:id="#+id/partition_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="PART 1"
android:layout_margin="5dp"
android:textColor="#color/dark_blue"
android:fontFamily="#font/gotham_thin_font"
android:textStyle="bold"
/>
<TextView
android:id="#+id/items_left"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Items left : "
android:layout_margin="5dp"
android:textColor="#color/dark_blue"
android:fontFamily="#font/gotham_thin_font"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"/>
<TextView
android:id="#+id/product_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#id/partition_text"
app:layout_constraintBottom_toTopOf="#id/items_left"
android:layout_margin="5dp"
android:textColor="#color/dark_blue"
android:fontFamily="#font/gotham_thin_font"
android:textStyle="bold"
android:gravity="start"
android:text="Product name here"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
My addapter
class PartitionRecycleAdapter : RecyclerView.Adapter<PartitionRecycleAdapter.ViewHolder>() {
private var partitions: List<Selection<Partition>> = listOf()
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
private var buttonManage: Job? = null
fun update(partitions: List<Selection<Partition>>) {
this.partitions = partitions
notifyDataSetChanged()
}
inner class ViewHolder(val binding: PartitionRecycleItemBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(partition: Selection<Partition>, position: Int) {
binding.apply {
partitionText.text = "Part ${partition.item.number}"
productName.text = partition.item.productName
val itemsLeftVar = partition.item.currentQty - partition.qty
itemsLeft.text = "Items left : $itemsLeftVar"
productCount.text = "X${partition.qty}"
if (partition.qty > 0) {
productCount.visibility = View.VISIBLE
} else {
productCount.visibility = View.INVISIBLE
}
Log.d(TAG, "bind: OUTER")
val listener = View.OnClickListener { view ->
buttonManage?.cancel()
buttonManage = coroutineScope.launch {
Log.d(TAG, "bind: Coroutine started")
delay(1000)
withContext(Dispatchers.Main) {
binding.plusButton.visibility = View.INVISIBLE
binding.minusButton.visibility = View.INVISIBLE
Log.d(TAG, "bind: Coroutine Ended")
}
}
if (view == partCard) {
binding.plusButton.visibility = View.VISIBLE
binding.minusButton.visibility = View.VISIBLE
}
if (view == plusButton) {
Log.d(TAG, "bind: PLUS")
if (itemsLeftVar > 0) {
partition.qty++
// notifyItemChanged(position)
}
}
if (view == minusButton) {
Log.d(TAG, "bind: MINUS")
if (partition.qty > 0) {
partition.qty--
// notifyItemChanged(position)
}
}
}
partCard.setOnClickListener(listener)
plusButton.setOnClickListener(listener)
minusButton.setOnClickListener(listener)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = PartitionRecycleItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val partition = partitions[position]
holder.bind(partition, position)
}
override fun getItemCount(): Int {
return partitions.size
}
}
The problem is , when I add notifyItemChanged on my button click . Everything breaks. the button closes sometimes . sometimes not .
please help..
I'm in the process of fiddling my way to a working app using Kotlin and I have hit a roadblock when trying to implement OnClickListeners for my three buttons. I have my RecyclerView populate properly, but despite following the recommendations on this SO post (except in Kotlin) and following the documentation, though I am still having trouble getting the implementation to work.
The code below is my adapter class for the implementation.
class BrowseHabitsAdapter(private val habits: ArrayList<Habit>) :
RecyclerView.Adapter<BrowseHabitsAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.habit_card, parent, false)
return ViewHolder(itemView, object: HabitClickListener {
override fun onDecrease(position: Int) {
val streak = itemView.dayCounter.text.toString().toInt()
itemView.dayCounter.text = streak.dec().toString()
}
override fun onIncrease(position: Int) {
val streak = itemView.dayCounter.text.toString().toInt()
itemView.dayCounter.text = streak.inc().toString()
}
override fun onEdit(position: Int) {
TODO("Change Activity to Edit")
}
})
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val currentItem = habits[position]
holder.habitTitle.text = currentItem.title
holder.streak.text = currentItem.streak.toString()
}
override fun getItemCount() = habits.size
class ViewHolder(itemView : View, listener : HabitClickListener) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
val habitTitle: TextView = itemView.habitTitle
val streak: TextView = itemView.dayCounter
val decreaseCounterButton : Button = itemView.decreaseCounterButton
val increaseCounterButton : Button = itemView.increaseCounterButton
val listener = listener
init {
decreaseCounterButton.setOnClickListener(this)
increaseCounterButton.setOnClickListener(this)
}
override fun onClick(v: View?) {
when (itemView.id) {
itemView.decreaseCounterButton.id -> listener.onDecrease(this.layoutPosition)
itemView.increaseCounterButton.id -> listener.onIncrease(this.layoutPosition)
}
}
}
interface HabitClickListener {
fun onDecrease(position : Int)
fun onIncrease(position : Int)
fun onEdit(position : Int)
}
}
and the following is my XML code defining one of my cards:
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/cardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
app:cardBackgroundColor="#eeeeee"
app:cardCornerRadius="10dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="#+id/cardHeader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="#+id/habitTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginRight="10dp"
android:text="#string/default_card_title"
android:textSize="18sp" />
<Space
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1" />
<ImageView
android:id="#+id/settingsIcon"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="bottom"
android:layout_marginRight="10dp"
app:srcCompat="#android:drawable/ic_menu_manage" />
</LinearLayout>
<LinearLayout
android:id="#+id/cardControls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<Button
android:id="#+id/decreaseCounterButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="-"
android:textAllCaps="false"
android:textSize="30sp" />
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="#+id/dayCounter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:fontFamily="sans-serif-medium"
android:text="0"
android:textAlignment="center"
android:textSize="30sp"
android:textStyle="bold" />
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<Button
android:id="#+id/increaseCounterButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="+"
android:textSize="30sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
Any additional explanation that can be provided as to what I did wrong and what is going on in detail would be really appreciated!
You are in kotlin so need to implement View.OnClickListener you can directly use setOnClickListener on any view.
Inside your ViewHolder Class:
itemView.increaseCounterButton.setOnClickListener{
listener.onIncrease(this.layoutPosition)
}
itemView.decreaseCounterButton.setOnClickListener{
listener.onDecrease(this.layoutPosition)
}
It should be view?.id instead of itemView.id
override fun onClick(v: View?) {
when (v?.id) {
itemView.decreaseCounterButton.id -> listener.onDecrease(this.layoutPosition)
itemView.increaseCounterButton.id -> listener.onIncrease(this.layoutPosition)
}
}
Additionally, your code with bugs. You handle HabitClickListener only update UI, when you scroll your data will be update base on habits. It means it will be revert when you scroll. Make sure streak of model Habit is var
return ViewHolder(itemView, object: HabitClickListener {
override fun onDecrease(position: Int) {
habits[position].streak = habits[position].streak.dec()
itemView.dayCounter.text = shabits[position].streak.toString()
}
override fun onIncrease(position: Int) {
habits[position].streak = habits[position].streak.inc()
itemView.dayCounter.text = shabits[position].streak.toString()
}
override fun onEdit(position: Int) {
TODO("Change Activity to Edit")
}
})
Using LiveData<PagedList> to display a recycler view that hits a Room database. When I do a search on screen, the number of items returned is less, however, there seems to be a lot of extra white space between the recycler view and the other buttons. The recycler view itself shrinks, and then moves down on the screen.
Here is the way it looks before the data is refreshed (before I do a search):
Here is after:
Here is my adapter for the recyclerView:
class WorkPackagesRecyclerAdapter(
private val onWorkPackageClickListener: OnClickWorkPackage
) : PagedListAdapter<Workpackage,
WorkPackagesRecyclerAdapter.WorkPackagesViewHolder>(
REPO_COMPARATOR
) {
interface OnClickWorkPackage {
fun clickWorkPackage(workPackageId: String)
}
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): WorkPackagesViewHolder {
val inflater = LayoutInflater.from(viewGroup.context)
val binding = inflate<ItemWorkPackageBinding>(
inflater,
R.layout.item_work_package,
viewGroup,
false
)
return WorkPackagesViewHolder(binding, viewGroup.context)
}
override fun onBindViewHolder(holder: WorkPackagesViewHolder, position: Int)
{
getItem(position)?.let {
holder.bind(it)
}
}
inner class WorkPackagesViewHolder(
internal var binding: ItemWorkPackageBinding,
internal val context: Context
) : RecyclerView.ViewHolder(binding.root), KoinComponent {
fun bind(data: Workpackage) {
val itemWorkPackagesViewModel: ItemWorkPackagesViewModel by inject{ parametersOf(data)}
this.binding.listItem.setOnClickListener {
onWorkPackageClickListener.clickWorkPackage(data.id)
}
this.binding.viewmodel = itemWorkPackagesViewModel
this.binding.executePendingBindings()
}
}
companion object {
private val REPO_COMPARATOR =
object : DiffUtil.ItemCallback<Workpackage>() {
override fun areItemsTheSame(
oldItem: Workpackage,
newItem: Workpackage
): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(
oldItem: Workpackage,
newItem: Workpackage
): Boolean =
oldItem == newItem
}
}
Here is how I set up the PagedList:
val searchQuery: MutableLiveData<SearchAndSort> = MutableLiveData(
SearchAndSort("",
WorkpackagesRepository.Companion.SortedBy.WorkPackageNumber,
AscendDescend.ASC))
var workPackagesList = Transformations.switchMap(searchQuery) { searchQuery ->
val factory = workPackageStorageDao.searchWorkpackages(
searchQuery.searchText,
searchQuery.sortBy.type + " " + searchQuery.ascendDescend.text
)
val pagedListBuilder = LivePagedListBuilder<Int, Workpackage>(factory, pagingLimit)
pagedListBuilder.build()
}
Here is where I am observing the adapter:
workPackagesViewModel.workPackagesList.observe(this, Observer { wpList ->
wpList ?: return#Observer
adapter = WorkPackagesRecyclerAdapter(this)
adapter.submitList(wpList)
binding.workPackagesRecyclerView.adapter = adapter
adapter.notifyDataSetChanged()
})
Here is the layout of the recycler view
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="com.bechtel.pf.ui.workpackages.WorkPackagesViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="727dp"
tools:layout_editor_absoluteY="1dp">
<EditText
android:id="#+id/search_bar_edit_text"
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginTop="#dimen/work_package_margin"
android:hint="#string/work_packages_search_hint"
app:layout_constraintBottom_toBottomOf="#id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<ImageButton
android:id="#+id/search_bar_magnifying_glass"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginTop="#dimen/work_package_margin"
android:background="#drawable/icons_search"
app:layout_constraintBottom_toBottomOf="#id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<androidx.constraintlayout.widget.Guideline
android:id="#+id/guideline"
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="horizontal"
app:layout_constraintGuide_begin="#dimen/work_package_search_guideline_start"
/>
<com.google.android.material.button.MaterialButton
android:id="#+id/btnSortBy"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginTop="5dp"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:backgroundTint="#color/colorPrimary"
app:layout_constraintEnd_toStartOf="#+id/btnAscendDescend"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/guideline"
app:layout_constraintHorizontal_chainStyle="spread"
android:text="#string/work_packages_sort"
/>
<com.google.android.material.button.MaterialButton
android:id="#+id/btnAscendDescend"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:backgroundTint="#color/colorPrimary"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintEnd_toStartOf="#+id/btnFilterBy"
app:layout_constraintStart_toEndOf="#+id/btnSortBy"
app:layout_constraintTop_toBottomOf="#+id/guideline"
app:layout_constraintHorizontal_chainStyle="spread"
android:text="#string/work_packages_ascend"
android:onClick="#{() -> viewModel.ascendingDescending()}"
/>
<com.google.android.material.button.MaterialButton
android:id="#+id/btnFilterBy"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="10dp"
android:backgroundTint="#color/colorPrimary"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/btnAscendDescend"
app:layout_constraintTop_toBottomOf="#+id/guideline"
android:text="#string/work_packages_filter"/>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/workPackagesRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:layout_marginTop="75dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/btnFilterBy">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
Anyone ever seen anything like this before? I've also made sure that I am using layout_height = wrap_content in my recyclerview layout and item.xml files.
Screenshot inspector:
OK, it looks like I had set the recyclerview layout_constraintBottom_toBottomOf was set to 'parent' and since the total height of the recyclerview shrunk when the number of items shrunk, it was moving the view to the bottom of the screen. I also could get rid of the top margin attribute and the recycler view stayed below the buttons. So now I have:
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/workPackagesRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/btnFilterBy"/>
And it all looks good!
I'm trying to hide part of my view in recycler when user toggles switch.
But when i toggle switch, sometimes it behaves in a strange way and hides only part of my view. It happens every 4 switch clicks (show-hide-show-hide), and i can't manage to solve this bug. Any advices what am i doing wrong?
There are screenshots of properly displayed view and awkwardly hidden view. As you can see, at the second screenshot seekbar with 2 image views is not fully hidden.
UPDATED: The problem is solved. Problem was in include tag. It somehow works wrong, the same layout in item file works fine.
Here is code for hiding logic:
itemView.light_switch.setOnCheckedChangeListener { _, isChecked ->
itemView.brightness.visibility = if (isChecked) View.VISIBLE else View.GONE
lightCallback(lamp, isChecked)
}
And here is my layout (recycler item):
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:background="#1a1a1a">
<ImageView
android:id="#+id/light_icon_image_view"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="#drawable/prod_foot" />
<TextView
android:id="#+id/light_name_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginStart="8dp"
android:fontFamily="sans-serif-medium"
android:textColor="#ffffff"
android:textSize="16sp"
android:textStyle="normal"
app:layout_constraintBottom_toBottomOf="#+id/light_icon_image_view"
app:layout_constraintStart_toEndOf="#+id/light_icon_image_view"
app:layout_constraintTop_toTopOf="#+id/light_icon_image_view"
tools:text="Floodlight 24 368 (01)" />
<TextView
android:id="#+id/textView9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif"
android:text="No group"
android:textColor="#7fffffff"
android:textSize="14sp"
android:textStyle="normal"
app:layout_constraintStart_toStartOf="#+id/light_name_text_view"
app:layout_constraintTop_toBottomOf="#+id/light_name_text_view" />
<android.support.v7.widget.SwitchCompat
android:id="#+id/light_switch"
style="#style/SwitchCompatStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
app:layout_constraintBottom_toBottomOf="#+id/light_icon_image_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#+id/light_icon_image_view"
app:thumbTint="#android:color/white" />
<include
android:id="#+id/brightness"
layout="#layout/brightness_seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/light_icon_image_view">
</include>
<ImageView
android:id="#+id/light_gradient_circle"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_marginTop="40dp"
app:layout_constraintStart_toStartOf="#+id/light_icon_image_view"
app:layout_constraintTop_toTopOf="#+id/light_icon_image_view"
app:srcCompat="#drawable/badge_color_wheel_active_block" />
</android.support.constraint.ConstraintLayout>
Include layout is provided below (seekbar with 2 images):
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="#1a1a1a"
android:layout_height="wrap_content">
<ImageView
android:id="#+id/imageView"
android:layout_width="15dp"
android:layout_height="15dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="#+id/seekbar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="#+id/seekbar"
app:srcCompat="#drawable/ic_dim_min" />
<SeekBar
android:id="#+id/seekbar"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginEnd="11dp"
android:layout_marginStart="11dp"
android:layout_marginTop="8dp"
android:max="255"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/imageView2"
app:layout_constraintStart_toEndOf="#+id/imageView"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="#+id/imageView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:layout_constraintBottom_toBottomOf="#+id/seekbar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="#+id/seekbar"
app:srcCompat="#drawable/ic_dim_max" />
</android.support.constraint.ConstraintLayout>
UPDATED: Adapter full code
class IndividualLightsAdapter(private val context: Context,
private val data: MutableList<Lamp>,
private val lightCallback: (Lamp, Boolean) -> Unit,
private val lightBrightnessCallback: (lamp: Lamp, brightness: Int) -> Unit)
: RecyclerView.Adapter<IndividualLightsAdapter.LightViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LightViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.light_item_new, parent, false)
return LightViewHolder(view)
}
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: LightViewHolder, position: Int) {
holder.bind(position)
}
inner class LightViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(position: Int) {
val lamp = data[position]
itemView.light_icon_image_view.setImageResource(lamp.imageId)
itemView.light_name_text_view.text = lamp.customName
itemView.light_gradient_circle.visibility = if (lamp is SmartLamp) View.VISIBLE else View.INVISIBLE
itemView.light_switch.isChecked = lamp.isTurnedOn
itemView.light_switch.setOnCheckedChangeListener { _, isChecked ->
itemView.brightness.visibility = if (isChecked) View.VISIBLE else View.GONE
lightCallback(lamp, isChecked)
}
setSeekBar()
itemView.seekbar.progress = lamp.brightness
itemView.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
lightBrightnessCallback(lamp, progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
}
})
}
private fun setSeekBar() {
val gradient = GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT,
intArrayOf(ContextCompat.getColor(context, R.color.grad_start), (ContextCompat.getColor(context, R.color.grad_end))))
gradient.cornerRadius = context.resources.getDimension(R.dimen.corner_radius_outer)
val progressLayer = ClipDrawable(gradient, Gravity.START, ClipDrawable.HORIZONTAL)
val background = GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, intArrayOf(Color.BLACK, Color.BLACK))
background.cornerRadius = context.resources.getDimension(R.dimen.corner_radius_inner)
// TODO Calculate padding dynamically
val backgroundLayer = InsetDrawable(background,
context.resources.getDimension(R.dimen.padding_inset_left).toInt(),
context.resources.getDimension(R.dimen.padding_inset_top).toInt(),
context.resources.getDimension(R.dimen.padding_inset_right).toInt(),
context.resources.getDimension(R.dimen.padding_inset_bottom).toInt())
itemView.seekbar.thumb.setTint((ContextCompat.getColor(context, R.color.color_thumb)))
itemView.seekbar.progressDrawable = LayerDrawable(arrayOf<Drawable>(backgroundLayer, progressLayer))
}
}
}
You shouldn't hide the view that is an item of recyclerView.
It doesn't work properly because of its recycling mechanism.
You just need to remove/add the item to be able to do the same effect.
you should has the below methods on your RecyclerView adapter
private void deleteItem(int position) {
if (position != RecyclerView.NO_POSITION) {
myList.remove(position);
notifyItemRemoved(position);
}
}
private void addItem(int position, MyModel model) {
if (position != RecyclerView.NO_POSITION) {
myList.add(position, model);
notifyItemInserted(position);
}
}