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
Related
I am trying to implement a ViewGroup named MyRecyclerView that acts like a real RecyclerView. LayoutManager and ViewHolder are not implemented in MyReclerView, the whole layout process is done by MyRecyclerView on its own.
MyAdapter is a dummy interface for feeding data to MyRecyclerView:
val myAdapter = object: MyRecyclerAdapter{
override fun onCreateViewHolder(row: Int, convertView: View?, parent: ViewGroup): View {
val id = when(getItemViewType(row)) {
0 -> R.layout.item_custom_view0
1 -> R.layout.item_custom_view1
else -> -1
}
val resultView = convertView ?: layoutInflater.inflate(id, parent, false)
if(getItemViewType(row) == 1)
resultView.findViewById<TextView>(R.id.tv_item1).text = testList[row]
return resultView
}
override fun onBindViewHolder(row: Int, convertView: View?, parent: ViewGroup):View {
val id = when(getItemViewType(row)) {
0 -> R.layout.item_custom_view0
1 -> R.layout.item_custom_view1
else -> -1
}
val resultView = convertView ?: layoutInflater.inflate(id, parent, false)
if(getItemViewType(row) == 1)
resultView.findViewById<TextView>(R.id.tv_item1).text = testList[row]
return resultView
}
override fun getItemViewType(row: Int): Int = row%2
override fun getItemViewTypeCount(): Int {
return 2
}
override fun getItemCount(): Int = itemCount
override fun getItemHeight(row: Int): Int = itemHeight // fixed height
}
A diagram illustrates how I wish this MyRecyclerView to work out:
I did not override onMeasure in MyRecyclerView, since I could not determine how many items should be put on screen when data first loaded. And this job is handled in onLayout. Items are set fixed heights. When the sum of heights of items greater than the height of MyRecyclerView, no more items are layout.
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
if(mNeedRelayout || changed) {
mNeedRelayout = false
mItemViewList.clear() // mItemViewList cached all views on Screen
removeAllViews()
if (mAdapter != null) {
// heights of each item is fixed (as 80dp) and accessed by MyAdapter
for(i in 0 until mItemCount)
mHeights[i] += mAdapter!!.getItemHeight(i)
// since onMeasure is not implemented
// mWidth: the width of MyRecyclerView
// mHeight: the height of MyRecyclerView
mWidth = r - l
mHeight = b - t
var itemViewTop = 0
var itemViewBottom = 0
// foreach whole data list, and determine how many items should be
// put on screen when data is first loaded
for(i in 0 until mItemCount){
itemViewBottom = itemViewTop + mHeights[i]
// itemView is layout in makeAndSetUpView
val itemView = makeAndSetupView(i, l,itemViewTop,r,itemViewBottom)
// there is no gap between item views
itemViewTop = itemViewBottom
mItemViewList.add(itemView)
addView(itemView, 0)
// if top of current item view is below screen, it should not be
// displayed on screen
if(itemViewTop > mHeight) break
}
}
}
}
I know itemView returned by makeAndSetUpView is a ViewGroup, and I call layout on it, all of its children will be measured and layout too.
private fun makeAndSetupView(
index: Int,
left: Int,
top: Int,
right: Int,
bottom: Int
): View{
// a simple reuse scheme
val itemView = obtain(index, right-left, bottom-top)
// layout children
itemView.layout(left, top, right, bottom)
return itemView
}
When I scroll down or up MyRecyclerView, Views out of screen will be reused and new data (text strings) are set in TextView, these views fill the blank. This behavior is carried out via relayoutViews:
private fun relayoutViews(){
val left = 0
var top = -mScrollY
val right = mWidth
var bottom = 0
mItemViewList.forEachIndexed { index, view ->
bottom = top + mHeights[index]
view.layout(left, top, right, bottom)
top = bottom
}
// Relayout is finished
mItemViewList.forEachIndexed { index, view ->
val lastView = (view as ViewGroup).getChildAt(0)
Log.d("TEST", " i: $index top: ${lastView.top} bottom: ${lastView.bottom} left: ${lastView.left} right: ${lastView.right}")
Log.d("TEST", " i: $index width: ${lastView.width} height: ${lastView.height}")
}
}
Debug output here is normal, views in cached mItemViewList seem to carry everything needed. And the text string is also set into TexView in the ItemView ViewGroup.
But it is so confusing that it seems no TextView is even created when I scroll and reuse these views.
ItemViews here are wrapped by a RelativeLayout:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="#dimen/item_height">
<TextView
android:text=" i am a fixed Text"
android:id="#+id/tv_item0"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
But when I just changed it to LinearLayout, everything is ok and views are reused and text strings are set.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="#dimen/item_height"
android:orientation="vertical"
android:gravity="center"
android:layout_marginTop="2dp">
<TextView
android:text="i am a fixed text"
android:id="#+id/tv_item0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
Can anyone point out what I did wrong here? I am looking forward to your enlightment.
I made a custom tab item for my tab layout and initialized it using view binding as follows:
val tabView = CustomTabBinding.inflate(LayoutInflater.from(mContext), null, false)
tabView.tvCustomTabTitle.text = it.title
tabView.tvCustomTabCount.visibility = View.GONE
Now when the user selects/unselects the tab I want to change the appearance of this custom view. Usually I achieved this using kotlin synthetics as follows:
fun setOnSelectView(tabLayout: TabLayout, position: Int = 0) {
val tab = tabLayout.getTabAt(position)
val selected = tab?.customView
if (selected != null)
selected.tv_custom_tab_title?.apply {
setTextColor(mContext.getColorCompat(R.color.colorAccent))
typeface = setFont(true)
}
selected?.tv_custom_tab_count?.apply {
setBackgroundResource(R.drawable.bullet_accent)
mContext.getColorCompat(android.R.color.white)
}
}
But now how do I achieve this using view binding?
I am using the method of findViewById():
fun Context.setOnSelectView(tabLayout: TabLayout, position: Int = 0) {
val tab = tabLayout.getTabAt(position)
val selected = tab?.customView
if (selected != null){
val title = selected.findViewById<TextView>(R.id.tv_custom_tab_title)
val count = selected.findViewById<TextView>(R.id.tv_custom_tab_count)
title.apply {
setTextColor(getColorCompat(R.color.colorAccent))
typeface = setFont(true)
}
count.apply {
setBackgroundResource(R.drawable.bullet_accent)
getColorCompat(android.R.color.white)
}
}
}
but I am hoping there is a better way to do this. If yes, then please do help me out.
Late reply but this is how I used view binding for custom tab layout, hope it helps
custom_tab.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="#+id/tv_custom_tab_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/tv_custom_tab_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
your activity/fragment:
val tab = binding.tabLayout.getTabAt(position)
tab.setCustomView(R.layout.custom_tab)
val tabBinding = tab.customView?.let {
CustomTabBinding.bind(it)
}
tabBinding?.tvCustomTabTitle?.text = "your title here"
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)
}
I'm updating the font size of all the texts on the app, what I want to achieve is, when I select the font size, i should be able to update the font sizes of all the texts on that activity.
My only problem is i can't find the size property on the Spinner Object.
This is what I did for Text Views, is it possible to apply a code similar to this one for Spinners ?
const val HEADER_TEXT = 24
const val NORMAL_TEXT = 14
private fun updateAssetSize(textView: TextView, additionalSize: Int, type: Int) {
val size = additionalSize + type
textView.setTextSize(COMPLEX_UNIT_SP, size.toFloat());
}
//calling the method:
updateAssetSize(screenText, additionalFontSize, HEADER_TEXT)
Note: This should be done from code, since this will be updated on run time.
Based on #Zain Suggestion, I resolved this by using an adapterlist object. Instead of using String I created a custom class with fontSize and text properties in it.
class SpinnerItem(
val text: String,
var fontSize: Int
) {
// this is necessary, in order for the text to display the texts in the dropdown list
override fun toString(): String {
return text
}
}
Here's the AdapterList that I created:
class SpinnerItemListAdapter(
context: Context,
val resourceId: Int,
var list: ArrayList<SpinnerItem>
) : ArrayAdapter<SpinnerItem>(context, resourceId, list) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val text = this.list[position].text
val size = this.list[position].fontSize
val inflater = LayoutInflater.from(context)
val convertView = inflater.inflate(resourceId, parent, false)
val simpleTextView = convertView.findViewById(R.id.simpleTextView) as TextView
simpleTextView.text = text
simpleTextView.setTextSize(size.toFloat())
return convertView
}
// We'll call this whenever there's an update in the fontSize
fun swapList(list: ArrayList<SpinnerItem>) {
clear()
addAll(list)
notifyDataSetChanged()
}
}
Here's the custom XML File spinner_item.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/simpleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="left"
android:padding="12dp"
android:textSize="16sp" />
The Spinner to be updated:
var fontSizes = arrayListOf(
SpinnerItem("Small", NORMAL_TEXT, "Default"),
SpinnerItem("Normal", NORMAL_TEXT, "Default"),
SpinnerItem("Large", NORMAL_TEXT, "Default"),
SpinnerItem("Largest", NORMAL_TEXT, "Default")
)
var fontSizeAdapterItem = SpinnerItemListAdapter(
this,
R.layout.spinner_item,
toSpinnerItemList(fontSizes, newSize)
)
Here's What will happen when we update it:
private fun updateSpinnerSize(additional: Int) {
val newSize = additional + NORMAL_TEXT
fontSizes = toSpinnerItemList(fontSizes, newSize)
fontSizeAdapterItem?.let {
it.swapList(fontSizes)
}
}
private fun toSpinnerItemList(
list: ArrayList<SpinnerItem>,
newSize: Int
): ArrayList<SpinnerItem> {
val itemList = ArrayList<SpinnerItem>()
for (item in list) {
item.fontSize = newSize
itemList.add(item)
}
return itemList
}
I have a very wide image inside my ViewHolder. The default ImageView behavior using centerCrop is should the image like this -
But I want to crop it so it will focus on the left side like this -
Things that needs to take into consideration are the fact that this ViewHolder is being resized very often.
Here is my ViewHolder XML & Adapter. Please note that I used fitXY just for display purpose, this is not what I actually want so feel free to ignore that line. -
<?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:id="#+id/vendors_row_item_root_layout"
android:layout_width="152dp"
android:layout_height="match_parent">
<androidx.cardview.widget.CardView
android:id="#+id/search_image_contact_cardview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="5dp"
app:cardCornerRadius="8dp"
tools:layout_height="160dp">
<ImageView
android:id="#+id/vendors_row_item_imageview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitXY"
tools:src="#drawable/kikiandggcover" />
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
Adapter -
class VendorAdapter(private val miniVendorModels: List<MiniVendorModel>, private val context: Context) : RecyclerView.Adapter<VendorsHolder>() {
companion object {
const val EXTRA_VENDOR_MODEL = "EVM"
}
private val vendorsHoldersList = mutableListOf<VendorsHolder>()
override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): VendorsHolder {
val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.fragment_marketplace_vendor_row_item, viewGroup, false)
val vendorsHolder = VendorsHolder(view)
vendorsHoldersList.add(vendorsHolder)
vendorsHolder.rootLayout.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
vendorsHolder.rootLayout.updateLayoutParams {
this.width = 250 + bottom
}
}
vendorsHolder.rootLayout.pivotX = 0f
vendorsHolder.rootLayout.pivotY = vendorsHolder.rootLayout.measuredHeight.toFloat()
return vendorsHolder
}
override fun onBindViewHolder(vendorsHolder: VendorsHolder, i: Int) {
val model = miniVendorModels[i]
Picasso.get().load(model.bannerPicture).into(vendorsHolder.vendorImageView)
vendorsHolder.vendorImageView.setOnClickListener { v: View? ->
try {
val intent = Intent(context, VendorPageActivity::class.java)
intent.putExtra(EXTRA_VENDOR_MODEL, model)
context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(context, ResourceHelper.getString(R.string.marketplace_vendor_unavailable), Toast.LENGTH_SHORT).show()
}
}
}
override fun getItemCount(): Int = miniVendorModels.size
}
This should be a really simple thing but I am strugling with this one for HOURS on-end.
Has anyone an idea?