I am building a custom view that makes an HTTP request to a Rest API every 6 seconds and displays a different question with its possible answers. A Buff in this case would be the object that contains the question and its possible answers.
Right now, the views are injected, but, when setting the data (see setAnswer function), the first view shows the text from the last element of the answer list. The rest of the views don't show any text.
Buff object received from API
With this data, I should show a question with 3 possible answers: "No goals!", "One goal!", "Two or more" in that order.
{
"result": {
"id": 1,
"author": {
"first_name": "Ronaldo"
},
"question": {
"id": 491,
"title": "Kaio Jorge has 4 goals this tournament – I think he will score again today. What do you think?"
},
"answers": [
{
"id": 1163,
"buff_id": 0,
"title": "No goals!"
},
{
"id": 1164,
"buff_id": 0,
"title": "One goal!"
},
{
"id": 1165,
"buff_id": 0,
"title": "Two or more!"
}
]
}
}
This is how is displayed at the moment
First element displays the text from the 3rd answer and the other two are empty
BuffView.kt (my custom view)
class BuffView(context: Context, attrs: AttributeSet? = null): LinearLayout(context, attrs) {
private val apiErrorHandler = ApiErrorHandler()
private val getBuffUseCase = GetBuffUseCase(apiErrorHandler)
private var buffIdCount = 1
private val buffView: View
init {
buffView = inflate(context, R.layout.buff_view, this)
}
fun start() {
getBuff()
}
private fun getBuff() {
getBuffUseCase.invoke(buffIdCount.toLong(), object : UseCaseResponse<Buff> {
override fun onSuccess(result: Buff) {
displayBuff(result)
}
override fun onError(errorModel: ErrorModel?) {
//Todo: show error toast
Log.e("AppDebug", "onError: errorModel $errorModel")
}
})
val delay = 6000L
RepeatHelper.repeatDelayed(delay) {
if (buffIdCount < 5) {
buffIdCount++
getBuff()
}
}
}
private fun displayBuff(buff: Buff) {
setQuestion(buff.question.title)
setAuthor(buff.author)
setAnswer(buff.answers)
setCloseButton()
buffView.visibility = VISIBLE
}
private fun setQuestion(questionText: String) {
question_text.text = questionText
}
private fun setAuthor(author: Buff.Author) {
val firstName = author.firstName
val lastName = author.lastName
sender_name.text = "$firstName $lastName"
Glide.with(buffView)
.load(author.image)
.into(sender_image)
}
private fun setAnswer(answers: List<Buff.Answer>) {
val answersContainer = findViewById<LinearLayout>(R.id.answersContainer)
answersContainer.removeAllViews()
for(answer in answers) {
val answerView: View = LayoutInflater.from(answersContainer.context).inflate(
R.layout.buff_answer,
answersContainer,
false
)
answer_text?.text = answer.title
answersContainer.addView(answerView)
}
}
private fun setCloseButton() {
buff_close.setOnClickListener {
buffView.visibility = GONE
}
}
}
buff_view.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="#layout/buff_sender"/>
<include layout="#layout/buff_question"/>
<LinearLayout
android:id="#+id/answersContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
buff_answer.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="#drawable/light_bg"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<ImageView
android:id="#+id/answer_image"
android:layout_width="27dp"
android:layout_height="27dp"
android:layout_gravity="center_vertical"
android:src="#drawable/ic_generic_answer"
android:padding="4dp" />
<TextView
android:id="#+id/answer_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="4dp"
android:textColor="#color/test_color_dark"
tools:text="The keeper's right"
android:textStyle="bold" />
</LinearLayout>
Try to call invalidate() after addView() to tell the View it should re-render. Also you need to set text on answerView, so the method should look like this:
private fun setAnswer(answers: List<Buff.Answer>) {
val answersContainer = findViewById<LinearLayout>(R.id.answersContainer)
answersContainer.removeAllViews()
for(answer in answers) {
val answerView: View = LayoutInflater.from(answersContainer.context).inflate(
R.layout.buff_answer,
answersContainer,
false
)
answerView.answer_text?.text = answer.title
answersContainer.addView(answerView)
}
invalidate() // or invalidateSelf()
}
Another thing is that it's a very bad practice to call your api from the custom view. Executing the usecase should be on the ViewModel/Presenter etc. level. Then the buffs should be provided to the Fragment/Activity and then set in the CustomView. You are also not cancelling the request in some view-destroying callback like onDetachedFromWindow() which can lead to memory leaks
You cannot add the same view more than once without recreating its new variable, this is not as simple as one would think.
private fun setAnswer(answers: List<Buff.Answer>) {
val answersContainer = findViewById<LinearLayout>(R.id.answersContainer)
var viewlist = ArrayList()
answersContainer.removeAllViews()
for(answer in answers) {
val answerView: View = LayoutInflater.from(answersContainer.context).inflate(
R.layout.buff_answer,
answersContainer,
false
)
answer_text?.text = answer.title
viewlist.add(answerView)
}
for(view in viewlist)
answersContainer.addView(view)
}
Related
I have a VideoView (circle) and custom animation (Falling cards) on the screen, in Fragment, as in the screenshot:
I haven't figured out how to do it any other way, so I've inserted four CardViews, and I'm scrolling through them using AnimationUtils. In code it looks like this:
onViewCreated()
viewLifecycleOwner.lifecycleScope.launch {
// withContext(Dispatchers.Main) {
startAnimation()
// }
}
private var count = 0
private var cardsCount = 0
private var animTime = 0L
private suspend fun startAnimation() {
cardsCount = Utils.getScannerTime3(appsList.size).first
repeatCount = Utils.getScannerTime3(appsList.size).third
val cardsList = arrayListOf(binding.cardAnim, binding.cardAnim2, binding.cardAnim3, binding.cardAnim4)
repeat(repeatCount) {
cardsList.forEach {
if (count < cardsCount) {
cardAnimation(it.animCardView, it.appsName, it.appsIcon, appsList[count])
delay(300)
}
count++
}
}
}
private suspend fun cardAnimation(cardView: CardView, textView: TextView, imageView: ImageView, appInfo: AppInfo) {
val scaleUp = AnimationUtils.loadAnimation(requireActivity(), R.anim.cardview_loader_animation_best)
cardView.startAnimation(scaleUp)
scaleUp.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(p0: Animation?) {
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.Main) {
textView.text = Utils.refactorLongAppName(appInfo.appName)
imageView.setImageDrawable(appInfo.appIcon)
// lifecycleScope.launch {
for (i in 0..11) {
cardView.elevation = i.toFloat()
delay(i * 100L)
}
// }
}
}
}
override fun onAnimationEnd(p0: Animation?) {
}
override fun onAnimationRepeat(p0: Animation?) {
}
})
}
I launch VideoView in the standard way in onResume
fun startVideoAnim(view: VideoView, context: Context, resourcesRawId: Int) {
view.setVideoURI(Uri.parse("android.resource://" + context.packageName + "/" + resourcesRawId))
view.setOnPreparedListener {
it.isLooping = true
}
view.requestFocus()
view.start()
}
CardView:
<?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"
android:id="#+id/animCardView"
style="#style/Widget.CardView"
android:layout_width="match_parent"
android:layout_height="54dp"
android:layout_marginHorizontal="24dp"
android:layout_marginVertical="3dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<ImageView
android:id="#+id/appsIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginVertical="7dp"
android:layout_marginStart="7dp" />
<TextView
android:id="#+id/appsName"
style="#style/TextStyleBold700"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
android:layout_marginStart="55dp"
android:letterSpacing="0.025"
android:textColor="#color/text_dark"
android:textSize="16sp" />
</androidx.cardview.widget.CardView>
The problem is that the animation does not work as it should, I know that the animation is correct, because if I remove the VideoView completely, everything works as it should. If you simply do not show the video, the animation does not work as it should. I tried to replace VideoView with ExoPlayer and Lottie, although this does not suit me. Unsuccessfully. Please tell me what could be wrong and how to fix it? And if there are tips, ideas on how to make such an animation differently, write too.
I am learning kotlin flow in android. I want to basically instant search in my list and filter to show in reyclerview. I searched in google and found this amazing medium post. This post is basically search from google. I want to search item in list and show in reyclerview. Can someone guide me how can I start this. I am explanning in more detail
Suppose I have one SearchBox and one Reyclerview which one item abc one, abc two, xyz one, xyz two... etc.
main image when all data is combine
Scenario 1
when I start typing in SearchBox and enter small a or capital A I want to show only two item matching in recyclerview, look like this
Scenario 2
when I enter any wrong text in SearchBox I want to basically show a text message that not found, look like this
Any guidance would be great. Thanks
I am adding my piece of code
ExploreViewModel.kt
class ExploreViewModel(private var list: ArrayList<Category>) : BaseViewModel() {
val filteredTopics = MutableStateFlow<List<opics>>(emptyList())
var topicSelected: TopicsArea? = TopicsArea.ALL
set(value) {
field = value
handleTopicSelection(field ?: TopicsArea.ALL)
}
private fun handleTopicSelection(value: TopicsArea) {
if (value == TopicsArea.ALL) {
filterAllCategories(true)
} else {
filteredTopics.value = list.firstOrNull { it.topics != null && it.title == value.title }
?.topics?.sortedBy { topic -> topic.title }.orEmpty()
}
}
fun filterAllCategories(isAllCategory: Boolean) {
if (isAllCategory && topicSelected == TopicsArea.ALL && !isFirstItemIsAllCategory()) {
list.add(0, code = TopicsArea.ALL.categoryCode))
} else if (isFirstItemIsAllCategory()) {
list.removeAt(0)
}
filteredTopics.value = list.flatMap { it.topics!! }.distinctBy { topic -> topic.title }.sortedBy { topic -> topic.title }
}
private fun isFirstItemIsAllCategory() = list.firstOrNull()?.code == TopicsArea.ALL
}
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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.SearchView
android:id="#+id/searchView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="16dp"
app:closeIcon="#drawable/ic_cancel"
app:layout_constraintBottom_toTopOf="#+id/exploreScroll"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintVertical_chainStyle="packed" />
<HorizontalScrollView
android:id="#+id/exploreScroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/searchView">
<com.google.android.material.chip.ChipGroup
android:id="#+id/exploreChips"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:chipSpacingHorizontal="10dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:singleLine="true"
app:singleSelection="true" />
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/exploreList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="20dp"
android:paddingTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintVertical_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/exploreScroll" />
</androidx.constraintlayout.widget.ConstraintLayout>
Category.kt
#Parcelize
data class Category(
val id: String? = null,
val title: String? = null,
val code: String? = null,
val topics: List<Topics>? = null,
) : Parcelable
Topics.kt
#Parcelize
data class Topics(
val id: String? = null,
val title: String? = null
) : Parcelable
Dummy data and coming from server
fun categoriesList() = listOf(
Categories("21", "physical", listOf(Topics("1", "Abc one"), Topics("2", "Abc Two"))),
Categories("2211", "mind", listOf(Topics("1", "xyz one"), Topics("2", "xyz two"))),
Categories("22131", "motorized", listOf(Topics("1", "xyz three"), Topics("2", "xyz four"))),
)
In my view model list is holding above dummy data. And In my recyclerview I am passing the whole object and I am doing flatMap to combine all data into list. Make sure In recyclerview is using Topic and using title property. In Image Abc one, Abc two is holding in Topic. Thanks
After #Tenfour04 suggestion I will go to A2 suggestion because I have already data which converted into flow and passing in my adapter. I am adding my activity code as well.
ExploreActivity.kt
class ExploreActivity : AppCompatActivity() {
private val binding by lazy { ExploreLayoutBinding.inflate(layoutInflater) }
val viewModel by viewModel<ExploreViewModel> {
val list = intent?.getParcelableArrayListExtra(LIST_KEY) ?: emptyList<Category>()
parametersOf(list)
}
var exploreAdapter = ExploreAdapter { topic -> handleNextActivity(topic) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupView()
}
fun setupView() {
setupSearchView()
setupFilteredTopic()
setupExploreAdapter()
}
private fun setupFilteredTopic() {
lifecycleScope.launchWhenCreated {
repeatOnLifecycle(Lifecycle.State.CREATED) {
viewModel.filteredTopics.collect { filteredTopicsList ->
exploreAdapter.submitList(filteredTopicsList)
}
}
}
}
fun setupSearchView() {
binding.searchView.apply {
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?) = false
override fun onQueryTextChange(newText: String?): Boolean {
return true
}
})
}
}
fun setupExploreAdapter() {
with(binding.exploreList) {
adapter = exploreAdapter
}
}
}
UPDATE 2
ExploreViewModel.kt
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.combine(filteredTopics) { queryText, categoriesList ->
val criteria = queryText.lowercase()
if (criteria.isEmpty()) {
return#combine filteredTopics
} else {
categoriesList.filter { category -> category.title?.lowercase()?.let { criteria.contains(it) } == true }
}
}
I am getting error when I set in adapter
fixed
filteredTopics.value
The tutorial you linked has a Flow produced by the SearchView. If you want to keep the search functionality in your ViewModel, you can put a MutableStateFlow in your ViewModel that will be updated by the SearchView indirectly. You can expose a property for updating the query.
There are two different ways this could be done, depending on whether you (A) already have a complete list of your data that you want to query quickly or (B) you want to query a server or your database every time your query text changes.
And then even (A) can be broken up into: (A1) you have a static plain old List, or (A2) your source List comes from a Flow, such as a returned Room flow that is not based on query parameters.
All code below is in the ViewModel class.
A1:
private val allCategories = categoriesList()
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.map {
val criteria = it.lowercase()
allCategories.filter { category -> criteria in category.title.lowercase }
}
A2:
In this example I put a simple placeholder flow for the upstream server query. This could be any flow.
private val allCategories = flow {
categoriesList()
}
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(200) // low debounce because we are just filtering local data
.distinctUntilChanged()
.combine(allCategories) { queryText, categoriesList ->
val criteria = queryText.lowercase()
categoriesList.filter { category -> criteria in category.title.lowercase }
}
B
private val query = MutableStateFlow("")
// You should add an OnQueryTextListener on your SearchView that
// sets this property in the ViewModel
var queryText: String
get() = query.value
set(value) { query.value = value }
// This is the flow that should be observed for the updated list that
// can be passed to the RecyclerView.Adapter.
val filteredCategories = query
.debounce(500) // maybe bigger to avoid too many queries
.distinctUntilChanged()
.map {
val criteria = it.lowercase()
categoriesList(criteria) // up to you to implement this depending on source
}
The expected behavior is to have a multiple drop down menu with mutual exclusion.
Example, I have:
array = ["A", "B", "C", "D", "E"]
menu1
menu2
menu3
If in menu1 I choose "A" then in menu2 and menu3 "A" doesn't have to appear.
MainActivity
class MainActivity : AppCompatActivity() {
private lateinit var scdd1: SingleCheckBoxDropDown
private lateinit var scdd2: SingleCheckBoxDropDown
private lateinit var scdd3: SingleCheckBoxDropDown
private val strArr = ArrayList<String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
strArr.add("string 1")
strArr.add("string 2")
strArr.add("string 3")
strArr.add("string 4")
strArr.add("string 5")
/**
* =============
* Single Select
* =============
*/
/*val scdd: SingleCheckBoxDropDown = findViewById(R.id.scdd1)
scdd.setData(strArr)
val tv1: TextView = findViewById(R.id.tv1)
scdd.setOnItemClickListenerWithSingleSelect {
val checked = scdd.getChecked()
if (checked != -1)
tv1.text = strArr[checked]
}*/
/**
* ====================================
* Single Select with Mutual Exclusion
* ====================================
*/
scdd1 = findViewById(R.id.category1)
scdd2 = findViewById(R.id.category2)
scdd3 = findViewById(R.id.category3)
scdd1.setData(strArr)
scdd2.setData(strArr)
scdd3.setData(strArr)
/** 1 */
scdd1.setOnItemClickListenerWithSingleSelect {
val filter1 = scdd1.getCheckedStr()
val filter2 = scdd2.getCheckedStr()
val filter3 = scdd3.getCheckedStr()
scdd1.setAnswer(filter1)
val data2 = strArr.filter { it != filter1 && it != filter3 }
val data3 = strArr.filter { it != filter1 && it != filter2 }
scdd2.setData(data2)
scdd3.setData(data3)
scdd1.setChecked(filter1)
scdd2.setChecked(filter2)
scdd3.setChecked(filter3)
}
/** 2 */
scdd2.setOnItemClickListenerWithSingleSelect {
val filter1 = scdd1.getCheckedStr()
val filter2 = scdd2.getCheckedStr()
val filter3 = scdd3.getCheckedStr()
scdd2.setAnswer(filter2)
val data1 = strArr.filter { it != filter2 && it != filter3 }
val data3 = strArr.filter { it != filter1 && it != filter2 }
scdd1.setData(data1)
scdd3.setData(data3)
scdd1.setChecked(filter1)
scdd2.setChecked(filter2)
scdd3.setChecked(filter3)
}
/** 3 */
scdd3.setOnItemClickListenerWithSingleSelect {
val filter1 = scdd1.getCheckedStr()
val filter2 = scdd2.getCheckedStr()
val filter3 = scdd3.getCheckedStr()
scdd3.setAnswer(filter3)
val data1 = strArr.filter { it != filter2 && it != filter3 }
val data2 = strArr.filter { it != filter1 && it != filter3 }
scdd1.setData(data1)
scdd2.setData(data2)
scdd1.setChecked(filter1)
scdd2.setChecked(filter2)
scdd3.setChecked(filter3)
}
/**
* Bug 1
*
* [x] 1 [ ] 1
* [ ] 4 --> [x] 4
* [ ] 5 [ ] 5
*
* [x] 2 [x] 1
* [ ] 4 [ ] 2
* [ ] 5 [ ] 5
*
* [x] 3 [x] 1
* [ ] 4 [ ] 3
* [ ] 5 [ ] 5
*/
}
}
activity_main.xml
<ScrollView 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="#002E2A"
android:gravity="center_horizontal"
android:padding="20dp"
tools:context=".MainActivity">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<jk.singlecheckboxdropdown.scdd.SingleCheckBoxDropDown
android:id="#+id/category1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/tv1"
app:my_string_entries="#array/arr_str"
app:question="Category 1" />
<jk.singlecheckboxdropdown.scdd.SingleCheckBoxDropDown
android:id="#+id/category2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/category1"
app:my_string_entries="#array/arr_str"
app:question="Category 2" />
<jk.singlecheckboxdropdown.scdd.SingleCheckBoxDropDown
android:id="#+id/category3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/category2"
app:my_string_entries="#array/arr_str"
app:question="Category 3" />
</RelativeLayout>
</ScrollView>
SingleCheckBoxDropDown
class SingleCheckBoxDropDown #JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr) {
private val scddContainer: RelativeLayout
private val dropdownMenu: RecyclerView
private val questionTV: TextView
private val answerTV: TextView
private var adapter: ScddAdapter
init {
LayoutInflater.from(context)
.inflate(R.layout.scdd, this, true) // true
/** GET CUSTOM ATTRIBUTES */
val typedArr = context.theme.obtainStyledAttributes(
attrs,
R.styleable.SingleCheckBoxDropDown,
defStyleAttr,
0
)
val question = typedArr.getString(R.styleable.SingleCheckBoxDropDown_question)
val charSequenceArr: Array<CharSequence> =
typedArr.getTextArray(R.styleable.SingleCheckBoxDropDown_my_string_entries)
scddContainer = findViewById(R.id.scdd_container)
dropdownMenu = findViewById(R.id.dropdown_menu)
answerTV = findViewById(R.id.answer)
questionTV = findViewById(R.id.question)
questionTV.text = question
scddContainer.setOnClickListener {
if (dropdownMenu.visibility == View.GONE)
dropdownMenu.visibility = View.VISIBLE
else dropdownMenu.visibility = View.GONE
}
adapter = ScddAdapter()
adapter.addAll(charSequenceArr)
dropdownMenu.layoutManager = LinearLayoutManager(context)
dropdownMenu.adapter = adapter
}
fun resetHard(data: List<String>) {
adapter = ScddAdapter()
adapter.addAll(data.toTypedArray())
dropdownMenu.adapter = adapter
}
fun setData(strArr: List<String>) {
/** bug 1 - hide bug */
// dropdownMenu.removeAllViews()
// dropdownMenu.removeAllViewsInLayout()
adapter.setData(strArr as ArrayList<String>)
// RecyclerView won't update view after data change.
Log.i("try", "strArr size: ${strArr.size}")
Log.i("try", "dropdownmenu size: ${dropdownMenu.size}")
}
fun setAnswer(filter: CharSequence) {
answerTV.text = filter
}
fun getCheckedStr(): CharSequence {
return adapter.getCheckedStr()
}
fun getChecked(): Int {
return adapter.getChecked()
}
/**
* Single Select Core
*/
fun setOnItemClickListenerWithSingleSelect(function: () -> Unit) {
adapter.setOnItemClickListener {
for (i in 0 until dropdownMenu.size)
if (i != it)
(dropdownMenu[i] as CheckBox).isChecked = false
function()
}
}
/** bug 1 - fix part 1 */
fun setChecked(filter: CharSequence) {
for (i in 0 until dropdownMenu.size) {
(dropdownMenu[i] as CheckBox).isChecked = false
if ((dropdownMenu[i] as CheckBox).text == filter) {
(dropdownMenu[i] as CheckBox).isChecked = true
// adapter.notifyItemChanged(i)
}
}
}
}
scdd.xml
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="#android:color/transparent"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<RelativeLayout
android:id="#+id/scdd_container"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#android:color/holo_green_dark"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical">
<TextView
android:id="#+id/question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textColor="#android:color/white"
android:textSize="12sp"
tools:text="The Question" />
<TextView
android:id="#+id/answer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:gravity="end"
android:textColor="#android:color/white"
tools:text="The Answer" />
</RelativeLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/dropdown_menu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#id/scdd_container"
android:background="#android:color/holo_green_light"
android:translationZ="999dp"
android:visibility="visible" />
</RelativeLayout>
ScddAdapter
/** ADAPTER */
class ScddAdapter : RecyclerView.Adapter<ScddAdapter.ScddViewHolder>() {
/** VIEW HOLDER */
class ScddViewHolder(view: View) :
RecyclerView.ViewHolder(view) {
val cb: CheckBox = view.findViewById(R.id.cb)
}
private var callback = { }
private var list = ArrayList<CharSequence>()
private var checked: Int = -1
private var checkedStr = ""
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScddViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_single_checkbox, parent, false) // false
return ScddViewHolder(view)
}
override fun onBindViewHolder(holder: ScddViewHolder, position: Int) {
holder.cb.text = list[position]
holder.itemView.setOnClickListener {
if(holder.cb.isChecked) {
checkedStr = holder.cb.text as String
checked = holder.layoutPosition
}
else {
checkedStr = ""
checked = -1
}
this.callback()
}
}
override fun getItemCount(): Int {
return list.size
}
fun addAll(charSequenceArr: Array<CharSequence>) {
list.addAll(charSequenceArr)
}
fun setData(datas: ArrayList<String>) {
// for (i in 0 until strArr.size) Log.i("try", strArr[i])
list.clear()
notifyDataSetChanged()
this.list.addAll(datas)
notifyDataSetChanged()
}
fun getChecked(): Int {
return checked
}
fun getCheckedStr(): CharSequence {
return checkedStr
}
fun setOnItemClickListener(function: (holderPosition: Int) -> Unit) {
this.callback = {
function(checked)
}
}
}
item_single_checkbox.xml
<CheckBox
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
style="?android:attr/spinnerItemStyle"
tools:text="hello"
android:id="#+id/cb"
android:singleLine="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:textColor="#color/white"
android:background="#android:color/background_dark"
android:buttonTint="#color/white"
android:textAlignment="inherit"/>
The problem that i noticed is that in setData() in SingleCheckBoxDropDown:
Log.i("try", "strArr size: ${strArr.size}")
Log.i("try", "dropdownmenu size: ${dropdownMenu.size}")
My RecyclerView (dropdownmenu) does not return the right size, idk why.
It seems to be because your adapter's addAll() method does not notify for changes.
Try changing it to:
fun addAll(charSequenceArr: Array<CharSequence>) {
val oldSize = getItemCount();
list.addAll(charSequenceArr)
notifyItemRangeInserted(oldSize, charSequenceArr.size())
}
Also, in setData() you can call notifyDataSetChanged() at the end only.
I develop a simple table-app, and the problem is, that the switch-box will change the state or doesn't reset the state correcly on change rotation.
befor rotating it looks like
this
after rotating it looks like
this
The hint state will set correct on change but the box is not displayed correctly. What has gone wrong? Actually the views should be updated on any changes in the same way...
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="wrap_content"
android:layout_height="wrap_content">
<Switch
android:id="#+id/tv_outputItemBoolean"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:trackTint="#color/switch_track_selector"
android:thumbTint="#color/switch_thumb_selector"
android:clickable="true"
android:paddingLeft="15dp"
android:paddingRight="15dp"
android:paddingBottom="2.5dp"
android:paddingTop="2.5dp"
android:textSize="20sp"
android:gravity="center"
/>
</LinearLayout>
fun getView(position: Int, rowView: TableRow?, parent: TableLayout): TableRow {
val rowView = TableRow(parent!!.context)
val rowData = this.getItem(position)
for (i in 0..columnCounts - 1) {
var data: ColumnData<*>? = rowData[i]
rowView.addView(this.getColumnView(data, null, rowView))
}
return rowView
}
private fun getColumnView(
col: ColumnData<*>?,
resycleView: View?,
parent: ViewGroup
): View {
return when (col?.type) {
DataType.TimeStamp -> {
val view = TimeStampPicker.View(parent.context)
view.timeStampPicker.timeStampInMillis = col.data as Long
view
}
DataType.Boolean ->
{
val c= parent.context
val view =
LayoutInflater.from(c)
.inflate(R.layout.output_item_boolean, null)
val switch: Switch = view.findViewById(R.id.tv_outputItemBoolean)
val value = col.data as Boolean
var state = switch.isChecked
Log.v("XXXXXXXXXXXXXXXX", value.toString()+","+state.toString())
switch.isChecked=value
state = switch.isChecked
Log.v("XXXXXXXXXXXXXXXX", value.toString()+","+state.toString())
switch.hint=c.getText(DataType.stringResourceOf(value))
view
}
else -> {
val view =
LayoutInflater.from(parent.context)
.inflate(R.layout.ouput_item_simple_text, null)
val tv: TextView = view.findViewById(R.id.tv_outputItemSimpleText)
tv.text = if (col != null) col.data.toString() else "NULL"
view
}
}
}
the output log print shows the right state befor and after setting.
V/XXXXXXXXXXXXXXXX: false,false
V/XXXXXXXXXXXXXXXX: false,false
V/XXXXXXXXXXXXXXXX: true,false
V/XXXXXXXXXXXXXXXX: true,true
V/XXXXXXXXXXXXXXXX: true,false
V/XXXXXXXXXXXXXXXX: true,true
V/XXXXXXXXXXXXXXXX: false,false
V/XXXXXXXXXXXXXXXX: false,false
I have a button (id: readyButtonIntro) inside a layout (introscreen.xml) that i need to enable. To do that, i have another button inside the RecyclerView.ViewHolder.
This is my Layout to need access
introscreen.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:background="#color/colorWhite"
tools:context=".IntroScreenVC">
<LinearLayout
android:id="#+id/indicatorContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent" />
<Button
android:id="#+id/readyButtonIntro"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="#color/colorWhite"
android:textColor="#color/colorTerciary"
android:alpha="0"
android:enabled="false"
android:text="Ready"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
And the another button is inside into the ViewHolder
slide_item_container.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp"
>
<Button
android:id="#+id/addData"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="Agregar Datos"
android:background="#drawable/button_rounded2"
/>
</LinearLayout>
How can I enable from inside the class that listener the button?
class IntroSlideViewHolder(view: View) : RecyclerView.ViewHolder(view) {
init {
addData.setOnClickListener(View.OnClickListener {
//NEED TO ENABLE THE BUTTON
// val introScreen = IntroScreenVC()
// introScreen.readyButton()
}
}
I have a fun into IntroScreenVC but always have a error that its null, if a pass the context or view, do nothing.
fun readyButton(){
readyButtonIntro.isEnabled = true
}
Could you help me with this? I would really appreciate it.
Thank you very much!
Regards.
Edit:
I put the adapter and the ViewHolder for more information.
I ignored that because I didn't want to create confusion. Sorry for that..
Class Constructor
data class IntroSlide(val title: String, val description: String, val icon: Int, val firstButton: Boolean, val secondButton: Boolean, val thirdButton: Boolean)
IntroScreenVC.kt
class IntroScreenVC: AppCompatActivity() {
private val introSliderAdapter = IntroScreenAdapter(
listOf(
IntroSlide(
"title1",
"description1",
R.drawable.logo,
false,
false,
false
),
IntroSlide(
"title2",
"description2",
R.drawable.doggrooming,
true,
false,
false
),
IntroSlide(
"title3",
"description3",
R.drawable.introscreen3,
false,
true,
false
)
)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.introscreen)
introSliderViewPager.adapter = introSliderAdapter
}
}
IntroScreenAdapter.kt
class IntroScreenAdapter(private val introSlides: List<IntroSlide>) : RecyclerView.Adapter<IntroSlideViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IntroSlideViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val cellForRow = layoutInflater.inflate(R.layout.slide_item_container,parent,false)
return IntroSlideViewHolder(cellForRow)
}
override fun getItemCount(): Int {
return introSlides.size
}
override fun onBindViewHolder(holder: IntroSlideViewHolder, position: Int) {
holder.bind(introSlides[position])
}
}
class IntroSlideViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val textTitle = view.findViewById<TextView>(R.id.textTitle)
private val textDescription = view.findViewById<TextView>(R.id.textDescription)
private val imageIcon = view.findViewById<ImageView>(R.id.imageSlideIcon)
private val addData = view.findViewById<Button>(R.id.addData)
private val addPet = view.findViewById<Button>(R.id.agregarMascota)
val contexto = itemView.context;
fun bind(introSlide: IntroSlide) {
textTitle.text = introSlide.title
textDescription.text = introSlide.description
imageIcon.setImageResource(introSlide.icon)
addData.isEnabled = introSlide.firstButton
addPet.isEnabled = introSlide.thirdButton
}
}
init {
addData.setOnClickListener(View.OnClickListener {
//ADD A ALERTDIALOG AND WHEN PRESS OK NEED TO ENABLE THAT BUTTON
val mDialogView = LayoutInflater.from(contexto).inflate(R.layout.alertdialog_add_data,null)
val builder = AlertDialog.Builder(contexto)
builder.setView(mDialogView)
val dialog: AlertDialog = builder.create()
dialog.show()
dialog.getWindow()?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
mDialogView.agregarDatosOK.setOnClickListener {
//HERE I NEED TO ENABLE THE BUTTON
//readyButtonIntro(introscreen.xml)
}
}
}
Edit2:
This is what I do with sharedPreferences.
IntroScreenAdapter.kt
class IntroSlideViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val textTitle = view.findViewById<TextView>(R.id.textTitle)
private val textDescription = view.findViewById<TextView>(R.id.textDescription)
private val imageIcon = view.findViewById<ImageView>(R.id.imageSlideIcon)
private val addData = view.findViewById<Button>(R.id.addData)
private val addPet = view.findViewById<Button>(R.id.agregarMascota)
//INIT sharedPreferences
private val prefs: SharedPreferences = view.context.getSharedPreferences(getString(R.string.prefs_file), Context.MODE_PRIVATE)
val contexto = itemView.context;
fun bind(introSlide: IntroSlide) {
textTitle.text = introSlide.title
textDescription.text = introSlide.description
imageIcon.setImageResource(introSlide.icon)
addData.isEnabled = introSlide.firstButton
addPet.isEnabled = introSlide.thirdButton
}
}
init {
addData.setOnClickListener(View.OnClickListener {
val mDialogView = LayoutInflater.from(contexto).inflate(R.layout.alertdialog_add_data,null)
val builder = AlertDialog.Builder(contexto)
builder.setView(mDialogView)
val dialog: AlertDialog = builder.create()
dialog.show()
dialog.getWindow()?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
mDialogView.agregarDatosOK.setOnClickListener {
//HERE EDIT THE sharedPreferences
with (prefs.edit()) {
putBoolean("ready_button_enabled", true)
apply()
}
dialog.dismiss()
}
}
}
IntroScreenVC.kt
class IntroScreenVC: AppCompatActivity() {
private val introSliderAdapter = IntroScreenAdapter(
listOf(
IntroSlide(
"title1",
"description1",
R.drawable.logo,
false,
false,
false
),
IntroSlide(
"title2",
"description2",
R.drawable.doggrooming,
true,
false,
false
),
IntroSlide(
"title3",
"description3",
R.drawable.introscreen3,
false,
true,
false
)
)
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.introscreen)
introSliderViewPager.adapter = introSliderAdapter
}
//HERE PUT THE RESUME TO EXPECT THE SHOW AND ENABLE THE BUTTON
override fun onResume() {
super.onResume()
val prefs = getSharedPreferences(getString(R.string.prefs_file), Context.MODE_PRIVATE)
val buttonEnabled = prefs.getBoolean("ready_button_enabled", false)
readyButtonIntro.isEnabled = buttonEnabled
if (buttonEnabled) {
readyButtonIntro.alpha = 1f
}else {
readyButtonIntro.alpha = 0f
}
}
}
SOLUTION:
Into the Activity (IntroScreenVC)
class IntroScreenVC: AppCompatActivity(), IntroScreenAdapter.AdapterOnClick {
private val introSliderAdapter =
listOf(
...
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.introscreen)
introSliderViewPager.adapter = IntroScreenAdapter(introSliderAdapter, this)
}
...
override fun onClick() {
//HERE ENABLE AND SHOW THE BUTTON
readyButtonIntro.isEnabled = true
readyButtonIntro.alpha = 1f
}
And the into the Adapter and RecyclerView
class IntroScreenAdapter(private val introSlides: List<IntroSlide>, val adapterOnClick: AdapterOnClick) : RecyclerView.Adapter<IntroScreenAdapter.IntroSliderViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IntroScreenAdapter.IntroSliderViewHolder {
val layoutInflater = LayoutInflater.from(parent?.context)
val cellForRow = layoutInflater.inflate(R.layout.slide_item_container,parent,false)
return IntroSliderViewHolder(cellForRow)
}
...
inner class IntroSliderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
...
init {
addData.setOnClickListener(View.OnClickListener {
val mDialogView = LayoutInflater.from(contexto).inflate(R.layout.alertdialog_add_data,null)
val builder = AlertDialog.Builder(contexto)
builder.setView(mDialogView)
val dialog: AlertDialog = builder.create()
dialog.show()
dialog.getWindow()?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT));
mDialogView.agregarDatosOK.setOnClickListener {
//FINALLY HERE CHANGE THE BUTTON TO ENABLE :)
adapterOnClick.onClick()
}
}
}
}
As I understand your problem you have a class A that is trying to communicate (change something) in class B.
There are several options for solving this kind of problem, depending on your exact needs.
From the code you have provided the relation of your Layouts and classes is not clear enough to me to give a more precise answer.
First of all, I understand you are using a recycler view.
A recycler view can have many items, and I assume you want to be able to enable that button from each item.
In order to let your IntroScreen class communicate with your viewholder, you have to pass a reference to the ViewHolder constructor.
For this purpose you could implement a simple "callback pattern".
Here is an example for defining an interface (e.g. for a function that enables the button) and implementing the callback.
Have a read here to see a well-explained example in Java. In Kotlin you could do it the same way.
Here a summary of the implementation steps:
define interface EnableButtonCallback that implements an abstract method enableButton
let your InfoScreen class implement that interface (in which you enable the button)
pass your InfoScreen class to your RecyclerView adapter and then from your adapter to your ViewHolder
in your ViewHolder onClickListener call the interface method enableButton
Update 2020/08/11
I try to give suggestions based on your updated code.
In the intro screen you set your viewPager adapter, but it is still not clear where this property is coming from and where exactly it is displayed. I guess maybe you just cut out the parameter definition. However, I just assume you have your views set up properly and this is not a problem here. For using recycler view with viewPager I found some related information here.
I can not yet see your use case clearly yet. Are you adding data persistently? Then should your button in the IntroScreen be permanently enabled?
In this case probably SharedPreferences are a good choice for persisting this kind of information. Even when it doesn't need to be persisted. Reading one shared preference file is lightweight and quick enough to be done on the main thread.
I will give you an example implementation here:
Get a shared preferences object
val sharedPref = activity?.getSharedPreferences(
"intro_button_settings_file", Context.MODE_PRIVATE) // String with the key should be in your string resource file
Pass your sharedPref to your adapter and your viewHolder and write to it:
with (sharedPref.edit()) {
putBoolean("ready_button_enabled", true) // String with the key should be in your string resource file
commit()
}
in your IntroScreen check the setting
val readyButtonShouldBeEnabled = sharedPref.getBoolean("ready_button_enabled",
false) // defaults to false
If, after clicking your enable button (that sets the setting to true), you need to return to your IntroScreen activity: then you could enable your button in your activities onResume method
A different solution would be:
You check the setting in your IntroScreen onClick method.
Then you don't need to disable the button.
You just set:
// in your IntroScreen readyButtonIntro onClick method
val buttonEnabled = sharedPref.getBoolean("ready_button_enabled",
false)
if (!buttonEnabled) {
// optional: write a Toast to notify the user why the button is doing nothing (yet)
Toast.makeText(yourIntroScreenContext, "First agregar datos", Toast.LENGTH_SHORT).show()
return // onClick returns, so nothing else will happen when clicked
}
... // your code when the button **should** be enabled
If your button should be disabled again, simply save false to the setting.
Since I do not know more about your use case, this seems like an easy and quick solution to me. This way you do not need to bother with implementing an interface. Anyways, when clicking your button in your viewHolder there is no immediate action taking place in your IntroScreen activity. You still want the user to return to the IntroScreen and click the enabled button.
Then checking if your button was enabled just when clicking on it appears sufficient to me.