android data binding set layout_width from parent width - android

How to set the child view's width using data binding. The value to set is dynamic and it depends on the width of the parent layout.
item_bar.xml
<LinearLayout
android:id="#+id/barLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="#{model.usedBarWidthPercentage}"
android:layout_height="4dp"/>
</LinearLayout>
The model can give me a percentage. Ex, if the percentage is 40%, it means the width of the textview should be 40% of the parent layout.
I know the idea of using data binding adapters, but dont know how to do it with the parent layout's width.

You can create a class file, example MyBingding.class
#BindingAdapter({ "bindWidth" })
public static void bindWidth(TextView textView, double perc) {
//You can do something by java code here
textView.xxxxxxxx;
}
Then use bindWidth method in XML:
<TextView
app:bindWidth="#{model.usedBarWidthPercentage}"
android:layout_width="wrap_content"
android:layout_height="4dp"/>
Make sure the usedBarWidthPercentage's data type is same as bindWidth method's perc.

I'm afraid you will be disappointed on this one iori24. I tried for qhite a while to figure out how to accomplish this, but some variables can be done post draw and some have to be done pre draw. Binding is meant to be for post draw type of values. layout_width and layout_height are pre draw values that need to be set ahead of time.
Unfortunately there is no current way to do this. I found many articles on it and can confirm this statement. However, I managed to find ways to manipulate my UI with various match parents or weights to accomplish what I needed or adjust in code.
I would love if they brought data binding to layout_width and height, and I think they will eventually, but for now you will just have to get more creative with your design.
If you would like you could provide your full sample code for someone to modify and hand back with potential fixes, but I'm guessing you will need a code option here and not an XML option. Now namezhouyu posted a binding adapter option. This is a postdraw that will take the textview and potentially modify it's value after it has been drawn. This could work, but you may see strange behavior as the original size would be wrap_content and then each subsequent change would cause it to jump to correct size. But I do like his work around, it is a clever solution.
So if you are bound and determined to do it through binding, then I would follow namez houyu's option.

What I end up doing is follow #Raghunandan's suggestion
class ViewHolder(val binding: ItemViewBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(model: Model) {
Logger.d("START")
binding.model = model
val treeObserver = binding.viewLayout.viewTreeObserver
if (treeObserver.isAlive) {
treeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.viewLayout.viewTreeObserver.removeOnGlobalLayoutListener(this) // need to call viewTreeObserver again
val maxValue = binding.viewLayout.width
val usePerc = binding.model.getUsedBarWidthPercentage()
Logger.d("maxValue=$maxValue, usePerc=$usePerc")
binding.view.layoutParams.width = (maxValue * usePerc).toInt()
}
})
}
}
}

Related

How do I create a button in kotlin without xml?

How do I make a button without any XML? I tried XML but it did not work and it is "Old" I heard.
Yeah, using XML is old but it's the standard way of defining views in Android. Nowadays exist alternatives to that such as Jetpack Compose which takes a more React style when declaring the GUI where you write #Composable functions that produce a UI. Quite nice.
In any case you can create the views yourself programatically but it's much more tedious, less maintainable and imho 💩
With that said, from your activity you can create instances of any of the layouts that you would use in XML and then add more views into it:
class YourActivty: AppCompatActivity() {
override fun onCreate(...) {
val frameLayout = FrameLayout(this).apply {
// Configure the layout's properties
}
val button = AppCompatButton(this).apply {
// Configure all button's properties
}
frameLayout.addView(button)
// Indicate your activity to use the framelayout as its content
setContentView(frameLayout)
}
}

Android BindAdapter with custom attribute with custom values (similar to visible attribute)

I've successfully made my first bind adapter and I wish to know a bit more about it.
I want to know how to make an attribute that can get only specific strings for a different state for my view.
For example every view has the visibility attribute that it can be"gone", "visible", "invisible"
<TextView
android:id="#+id/loading_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="#id/inventory_items_recycler"
app:layout_constraintEnd_toEndOf="#+id/inventory_items_recycler"
app:layout_constraintStart_toStartOf="#id/inventory_items_recycler"
app:layout_constraintBottom_toBottomOf="#id/inventory_items_recycler"
android:textSize="18sp"
android:visibility="gone"
app:item_id="#{ItemID.BLACK_GLOVES.ordinal()}"
/>
I've made a custom attribute called item_id that get a number that represent enum value. And in my binding utils I have this code:
#BindingAdapter("item_id")
public static void setItemName(TextView tv, int itemId) {
tv.setText(ItemData.get(ItemID.values()[itemId]).getName());
}
I prefer to have something similar to the visibility attribute that it value can be either "visible", "invisible" or "gone"
Bonus::
I wish android studio can auto-complete me for the possibilities that I can use.
You could pass directly the enum to your binding adapter, instead of converting it first to an int and than back to enum.
#BindingAdapter("item_id")
public static void setItemName(TextView tv, ItemID itemId) {
..
}
Then you could pass directly the enum in your xml:
app:item_id="#{ItemID.BLACK_GLOVES}"
This way you'll have a limited number of possibilities to enter and will be less likely to accidentally enter a meaningless integer.
However, binding adapters and custom attibutes are different. With a binding adapter, you still need to use the syntax of binding expression, ie: "#{ }".
android:visibility , on the other hand, is an attribute. You can also define custom attributes for your custom views and get something similar (have a limited number of input options and IDE shows you your options etc). But you shouldn't confuse that with binding adapters. These are two different concepts.
Try this:
#BindingAdapter("isGone")
#JvmStatic
fun View.setVisibility(isGone: Boolean) {
if (isGone) this.visibility = View.GONE else View.VISIBLE
}
Inside your xml:
<com.google.android.material.checkbox.MaterialCheckBox
android:id="#+id/cb_class"
style="#style/TextStyleNormal.White"
android:layout_marginStart="#dimen/margin_large"
isGone="#{isSharedDailyActivity}"// it take boolean value
app:buttonTint="#color/white"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:useMaterialThemeColors="true" />

Android LayoutPosition is -1 causing OutOfBoundsException

I've created an app that has a list of cards within a RecyclerView that each have functionality of their own. I wanted to have each card choose the next color from an array defined in my colors.xml. In order to accomplish this, within my ViewHolder initialization, I set the background color of the card using cardContainer.setBackgroundColor(colors[this.layoutPosition % colors.size]. This would make it so that the colors would be cycled when more cards are created. However, I seem to be encountering the issue where my layout position is negative despite there being a set number of cards (25) created at the beginning.
While trying to search around and find the cause, I read here that if you call notifyDataSetChanged() the adapterPosition will become -1. While I am not using adapterPosition here, I thought that maybe it would be a similar issue, however, I am not adding any additional data at the time of the creation of the list items.
My ViewHolder code can be seen below. This is where the issue arises, but if any additional code is necessary feel free to ask.
class ViewHolder(itemView : View, private val listener: HabitClickListener) : RecyclerView.ViewHolder(itemView) {
val habitTitle: TextView = itemView.habitTitle
val streak: TextView = itemView.dayCounter
val cardContainer: LinearLayout = itemView.cardContainer
private val decreaseCounterButton : Button = itemView.decreaseCounterButton
private val increaseCounterButton : Button = itemView.increaseCounterButton
init {
chooseCardColor() // Choose the color for each card from the available colors
itemView.setOnClickListener {
listener.onCardClick(this.layoutPosition)
}
decreaseCounterButton.setOnClickListener {
listener.onDecrease(this.layoutPosition)
}
increaseCounterButton.setOnClickListener {
listener.onIncrease(this.layoutPosition)
}
}
private fun chooseCardColor() {
val colors = itemView.resources.getIntArray(R.array.cardColors)
cardContainer.setBackgroundColor(colors[this.layoutPosition % colors.size])
}
}
I will try to simplify this further, you should use the getAdapterPosition of ViewHolder
In recyclerview, storing the data and displaying the data are two separate things(Notice how you can use different managers(LinearLayoutManager, GridLayoutManager) to present the data in a different way.When some data changes in recyclerview, it notifies the ui to change what is shown in the screen. Even though it is really small, there is a delay between the change in the content of recyclerview and change in layout, that's why these two behave differently.
My information in this may be outdated but also don't just use the position variable as it can be inconsistent when another element is added/deleted to recyclerview due to how onBindViewHolder()(existing variables position wasn't updated when a new element is added/deleted) behaves. Instead use getAdapterPosition().
Edit: Quick fix if you don't want to deal with viewHolder gimmicks.
Add a new field to your custom object which decides what color it should be. Then make this calculation in your fragment/activity by looking at the index of your object in the list instead of doing the calculation in the viewHolder. Now you can set the color you want inside the viewHolderby looking at your object's new field. Of course you should be careful when adding/deleting a new object when you do this, but same holds true when you do it via viewHolder

ViewHolder attribute changes affects Objects attribute

private fun turnOnAllItems() {
items.forEachIndexed { index, item ->
val viewHolder = recyclerView.findViewHolderForAdapterPosition(index)
as SwitchableItemViewHolder
viewHolder.switchButton.isChecked = false
}
}
What this does, is it also changes list items object values isEnabled to false. Looks weird to me, as I actually change viewHolder attribute. Why is this happening? How to avoid this?
I strongly believe that you are doing it the wrong way. RecyclerView is meant to display already modified data, meaning that you have a set of it.
Let's say, 10 tables in restaurant, and at some point table #4 becomes available for new customer and you want to indicate that.
A good approach would be to modify your list of tables somewhere outside RCV, even fragment or activity will do, and then just graphically update (all or just one) item by means of RCV.
Here's a little article I made to illustrate how to properly use RecyclerView, hope it will help you

Fragments : what is the best place to measure Views?

I have a fragment and I need to measure location/width/height of its views on screen and pass to some other class.
So what I have is a function which does it, something like this :
private void measureTest(){
v = ourView.findViewById(R.id.someTextField);
v.getLocationOnScreen(loc);
int w = v.getWidth();
...
SomeClass.passLocation(loc,w);
...
The problem is that the location/width/height of views is not ready within fragment lifecycle.
So if I run that function within these lifecycle methods :
onCreateView
onViewCreated
onStart
onResume
I either get wrong location and width/height measurments or 0 values.
The only solution I found is to add a GlobalLayoutListener like this to mainView
mainView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
public void onGlobalLayout() {
if(alreadyMeasured)
mainView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
else
measureTest();
}
});
this gets the job done.. but its just Yack! IMO.
Is there a better way of doing this? seems like such a basic thing to do
inside onActivityCreated of your fragment retrieve the currentView (with getView()) and post a runnable to its queue. Inside the runnable invoke measureTest()
There is no better way. That code isn't that bad! It's fired as soon as the view is layed out (my terminology might be a bit weird there) which happens right after measuring. That is how it is done in the BitmapFun sample (see ImageGridFragment, line 120) in Google's Android docs. There is a comment on that particular piece of code stating:
// This listener is used to get the final width of the GridView and then calculate the
// number of columns and the width of each column. The width of each column is variable
// as the GridView has stretchMode=columnWidth. The column width is used to set the height
// of each view so we get nice square thumbnails.

Categories

Resources