I have a custom control where I need to create a number of ProgressBars with specific style, and add them to a LinearLayout container, with some even spacing (with the only modification after creation being setting the progress, and setting 0 starting margin on the first, and 0 ending margin for the last items).
I'm using the following code to create the ProgressBar instances:
if (container.childCount != progressSteps) {
container.removeAllViews()
repeat(progressSteps) {
container.addView(generateProgressBar())
}
container.children.first().apply {
layoutParams = (layoutParams as LinearLayout.LayoutParams).apply {
marginStart = 0
}
}
container.children.last().apply {
layoutParams = (layoutParams as LinearLayout.LayoutParams).apply {
marginEnd = 0
}
}
}
And this is the bit that's supposed to be updating the progress status of each:
(0 until container.childCount).forEach { index ->
val child = container[index]
val shouldTint = index < currentProgress
(child as ProgressBar).progress = if (shouldTint) 100 else 0
}
Explanation:
container is my LinearLayout meant to contain the ProgressBar instances
progressSteps and currentProgress are Int variable properties on my View class
generateProgressBar() creates a ProgressBar instance with some custom styling (basically using the android.R.attr.progressBarStyleHorizontal style, overriding the progressDrawable and some layout params)
What I end up with is the last ProgressBar getting tinted, however according to the LayoutInspector dump, the first two should be. But they stay gray.
Related
I have a RecyclerView made of CardView with several TextViews
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
<TextView...
<TextView...
...
</LinearLayout>
</androidx.cardview.widget.CardView>
I'm trying to change the width of a TextView to be equal in each RecyclerView row and to fit the widest content, so it looks like a table with equal columns. To do that I made a function:
private fun optimizeLayout() {
var maxWidth = 100
val recyclerView = myRecyclerViewLayout
recyclerView.doOnLayout {
// Get max width
for (i in 0 until recyclerView.childCount) {
val v = recyclerView.layoutManager?.findViewByPosition(i)
val tv = v?.findViewById<TextView>(R.id.txtDest)
if (tv != null) {
tv.measure(0, 0)
if (tv.measuredWidth > maxWidth) maxWidth = tv.measuredWidth
println(i.toString() + " " + tv.measuredWidth)
}
}
// Set width
for (i in 0 until recyclerView.childCount) {
val v = recyclerView.layoutManager?.findViewByPosition(i)
val tv = v?.findViewById<TextView>(R.id.txtDest)
if (tv != null) {
tv.width = maxWidth
println("set $i")
}
}
// Header width
txtHeader.doOnLayout { txtHeader.width = maxWidth }
}
}
This function was created after reading many other posts on a similar topic on the Internet. I call it from onViewCreated of the Fragment that contains the RecyclerView and it works fine beside that I get warnings:
requestLayout() improperly called by com.google.android.material.textview.MaterialTextView{b1b06f0 V.ED..... ......ID 0,5-143,110 #7f0a0225 app:id/txtDest} during layout: running second layout pass
I also have a dialog for editing items. The problem starts when I try to change an item - for example I enter wider text and want all the rows in the RecyclerView to have a new width. When the dialog closes I call the function. It works, but not for all elements(?!). For example, I have 10 rows and the function stops in fourth like recyclerView.childCount only returned 4 out of 10. When I close and open Fragment all columns are again even for all elements. I tried to run the function in thread and from onLayoutCompleted:
recyclerView.layoutManager = object : LinearLayoutManager(this.context) {
override fun onLayoutCompleted(state: RecyclerView.State?) {
optimizeLayout()
super.onLayoutCompleted(state)
}
}
val runnable = Runnable {
while (true) {
optimizeLayout()
Thread.sleep(1000)
}
}
Every time with the same result. Why is this happening?
You should never reference recycled views outside of onBindViewHolder.
The reason all of the recycler views are not updating is due to the views not being drawn yet when you call optimizeLayout(). A RecyclerView recycles views and only draws them when they become visible.
I suggest following google design guidelines for list patterns
Which would make the view match parent or keep a consistent width across all views.
If that is not an option I would loop through the string list and find the string with the greatest length and calculate the width and pass it to the adapter before setting the adapter list.
I have studied many questions and answers in Stackoverflow, then I made a dummy function, that I run after a click in my app.
Here is the code:
fun openDialog(cx: Context) {
val alertDialog = Dialog(cx)
var linLayout = LinearLayout(cx)
linLayout.setOrientation(LinearLayout.VERTICAL);
// Set width, height and weight
linLayout.layoutParams = LinearLayout.LayoutParams(500,1000,1F)
// top and left position
linLayout.x = 0F
linLayout.y = 0F
val title = TextView(cx); // dummy view 1
title.setText("Custom Dialog 1")
title.setTextColor(Color.BLACK)
title.setTextSize(20F)
linLayout.addView(title) // add in layout
val msg = TextView(cx) // dummy view 2
msg.setText("Custom Dialog Box 2")
msg.setTextColor(Color.RED)
msg.setTextSize(10F)
linLayout.addView(msg) // add in layout
alertDialog.setContentView(linLayout) // Add the layout in Dialog
alertDialog.show(); // Show the dialog with layout
}
What I get? A proper dialog, but in the middle of the screen with width and height defined by the content. I also try to use the windows linked to the custom dialog without success.
val wlp = win.attributes
wlp.apply {
x = 0
y = 0
height = 1000
width = 500
}
win.attributes = wlp
No changes. However wlp.gravity = Gravity.BOTTOM works, but it's not enough for me. A also try to use win.setLayout(1000,500) without success
The Android documentation states:
Set the width and height layout parameters of the window. The default
for both of these is MATCH_PARENT; you can change them to WRAP_CONTENT
or an absolute value to make a window that is not full-screen.
Why can’t I resize and position the layout that I assign to my dialog?
Has somebody a hint?
The cell phone screen:
Update
I've got to change the position of dialog using
wlp.gravity = Gravity.TOP or Gravity.LEFT
wlp.x = 100 // Relative to left
wlp.y = 200 // relative to top
I keep trying to figure out how to change the width and height.
Set the layout params for the view you're inflating when you set the content :
this line :
alertDialog.setContentView(linLayout)
should be :
alertDialog.setContentView(linLayout, LinearLayout.LayoutParams(500,1000,1F))
You can also remove the explicit setting of the params for the LinearLayout
TL;DR: Here's the gist of everything that I can think of that's relevant to the issue I'm facing: [GIST LINK]
And here's a picture of the problem
I'm trying to set up a number of buttons that will all grow to the same size as each other by equal weighting in a vertically oriented LinearLayout container.
The problem I'm facing surfaces when the text on these buttons cause a different number of lines per button.
Let's say n is the lowest line count for the buttons and m is the highest line count; any descenders in the text of buttons with line count m are cut off. Refer to the words "qshowing my clipping problem" in the linked screengrab, where all descenders are cut off.
How can I go about fixing this? The clipping gets much worse if I introduce android:lineSpacingExtra to the button style.
If it's relevant, my minimum API is set to 21
I've fixed this using RxJava to set the height programmatically to the correct maximum so that no clipping occurs. If there is a better solution I'll be glad to see it, but this is what is working for me for now:
class MyActivity {
// ...
private val compositeDisposable = CompositeDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.my_activity)
// ...
val container: LinearLayout = findViewById(R.id.container)
val numBtns = getNumBtnsToAdd()
val btnList: MutableList<Button> = mutableListOf()
val margin10 = dpToPx(10f).toInt()
val countDown = CountDownLatch(numBtns)
val desiredLp = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0).apply {
gravity = Gravity.CENTER
setMargins(margin10, margin10, margin10, margin10)
}
// Completable will be run once per subscriber and emit a success or error
val layoutCompletable = Completable.fromAction {
countDown.await()
for (btn in btnList) btn.layoutParams = desiredLp
}.subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread())
compositeDisposable.add(
layoutCompletable.subscribe({
Log.d("MyActivity", "Set LayoutParams on all buttons.")
}, Throwable::printStackTrace)
)
for (i in 0 until numBtns) {
val btn = Button(this, null, 0, R.style.button_style).apply {
layoutParams = LinearLayout.LayoutParams(desiredLp).apply { height = LinearLayout.LayoutParams.WRAP_CONTENTS }
text = if (i == 0) "Button${i+1} with short text"
else "Button${i+1} with text that will span multiple lines showing my clipping problem"
setOnClickListener { doSomething() }
}
val listener = object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
countDown.countDown()
val height = btn.height
if (height > desiredLp.height) desiredLp.height = height
btn.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
btn.viewTreeObserver.addOnGlobalLayoutListener(listener)
btnList.add(btn)
container.addView(btn)
}
// ...
}
override fun finish() {
compositeDisposable.clear()
super.finish()
}
// ...
}
My guess, the main cause is buttons have fixed size. More preciously, you use LinearLayout to share available room between buttons via weight attribute. You can see the single line button height is same with 2-lines button height. So 2-lines buttons are forced to clip the text.
According your XML file you want enable the vertical scroll when there is no more room. In this case you don't need to use weight attribute. Just buttons under each other with margins.
I've made an RecyclerView which is expandable , my expanded items in RecyclerView have different counts and it's not possible to set a single layout for them.
My program compares the price of some services ( 6 different services for now ) and every service has a different count of sub services which that count will be passed to RV Adapter.
I want somethings like this :
different expanded item counts
I've tried to solve it with these solutions :
FIRST SOLUTION :
my RV data model has a int variable named to serviceCount and gets data from MainActivity for each type of service, my layout should repeat as serviceCount size , I've written this code in onBindViewHolder :
if (holder.detailLayout.getVisibility() == View.VISIBLE) {
for (int i = 0; i < priceList.get(position).getServiceCount(); i++) {
// Code
}
I'm trying to create a layout programmatically and repeat it as that size which is something like this :
for (int i = 0; i < priceList.get(position).getServiceCount(); i++) {
ConstraintLayout newDetailLayout = new ConstraintLayout(context);
ConstraintLayout.LayoutParams layoutParams = new ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.MATCH_PARENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT);
layoutParams.topToBottom = R.id.tv_pricedetail_service;
layoutParams.rightToRight = 0;
layoutParams.leftToLeft = 0;
layoutParams.setMargins(0,margin8dp*i*6,0,0);
Button requestButton = new Button(context);
requestButton.setId(View.generateViewId());
requestButton.setText("درخواست" + " " + String.valueOf(i));
ConstraintLayout.LayoutParams requestButtonParams = new ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT);
requestButtonParams.leftToLeft = 0;
requestButtonParams.topToTop = 0;
requestButtonParams.setMargins(margin8dp *4,margin8dp *2,0,0);
newDetailLayout.addView(requestButton, requestButtonParams);
TextView serviceName = new TextView(context);
serviceName.setId(View.generateViewId());
serviceName.setText("تست" + " " + String.valueOf(i));
ConstraintLayout.LayoutParams serviceNameParams = new ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT);
serviceNameParams.topToTop = 0;
serviceNameParams.rightToRight = 0;
serviceNameParams.baselineToBaseline = requestButton.getId();
serviceNameParams.setMargins(0,margin8dp *2,margin8dp *4,0);
newDetailLayout.addView(serviceName, serviceNameParams);
TextView serviceCost = new TextView(context);
serviceCost.setText("هزینه" + " " + String.valueOf(i));
ConstraintLayout.LayoutParams serviceCostParams = new ConstraintLayout.LayoutParams(
ConstraintLayout.LayoutParams.WRAP_CONTENT,
ConstraintLayout.LayoutParams.WRAP_CONTENT);
serviceCostParams.leftToRight = requestButton.getId();
serviceCostParams.rightToLeft = serviceName.getId();
serviceCostParams.baselineToBaseline = requestButton.getId();
newDetailLayout.addView(serviceCost, serviceCostParams);
holder.detailLayout.addView(newDetailLayout, layoutParams);
}
//Toast.makeText(context, String.valueOf(priceList.get(position).getServiceCount()), Toast.LENGTH_SHORT).show();
}
output of my code is this : output view BUT when user expand the first item the other items in expanded view copy the first item expanded detail ! and I should create different layout for every expanded layout.
SECOND SOLUTION:
I've made 6 different layout for each service ( they will be more in future ) and inflate them in onCreateViewHolder with instantiated variables
is this right for doing something like this ? or I can do something better ?
EDIT :
onBindViewHolder Codes :
public void onBindViewHolder(#NonNull itemsViewHolder holder, int position) {
// Init Layout
final priceItemDM items = priceList.get(position);
holder.iv_logo.setImageDrawable(items.getLogo());
holder.txt_name.setText(items.getName());
holder.txt_price.setText(items.getPrice());
// Expand & Collapse Mode
final boolean isExpanded = position == mExpandedPosition;
final int positionNo = position;
holder.detailLayout.setVisibility(isExpanded ? View.VISIBLE : View.GONE);
holder.itemView.setActivated(isExpanded);
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (!items.getPrice().equals(receivingData) && !items.getPrice().equals(receiverError)) {
mExpandedPosition = isExpanded ? -1 : positionNo;
notifyItemChanged(positionNo);
} else {
Toast.makeText(context, "اطلاعات ناقص است ، لطفا مجددا تلاش فرمایید", Toast.LENGTH_SHORT).show();
}
}
});
if (!items.getPrice().equals(receivingData)) {
holder.pb_loading.setVisibility(View.INVISIBLE);
if (holder.detailLayout.getVisibility() == View.VISIBLE &&
!items.getPrice().equals(receiverError)) {
//priceList.get(position).getServiceNames().size()
holder.detailLayout.removeAllViews();
holder.detailLayout.addView(childView);
}
}
}
my expanded items in RecyclerView have different counts and it's not possible to set a single layout for them.
In your case it actually is possible to use a single layout for all items because they all look the same. They all can be expanded in the same way and their contents always look the same - only the amount of child items is different, but it doesn't mean you can't use one layout for them.
You should have two XML layout files - one for the expandable item, and one for the inner child row (the one that has a button).
The first solution is correct, you can't go with the second one. Creating a new layout every time makes no sense because your project will quickly turn into a mess due to the amount of files and the code that inflates them. Although the first solution doesn't have to be that complicated. I see that you are configuring all views in runtime - it would look much simpler if you do it in XML and just inflate the view when needed.
when user expand the first item the other items in expanded view copy the first item expanded detail ! and I should create different layout for every expanded layout.
I'm not sure I get your point but the approach is correct. The only thing is that you have to keep in mind this is a RecyclerView which reuses its child views when you scroll.
Example:
You expand item#1 and inflate 5 child rows in it, then you scroll. If item#4 is also expanded the recycler view will reuse item#1 when showing item#4, i.e. item#4 will automatically get 5 child rows even if it shouldn't have that many.
That means you have to clean up the child rows every time in onBindViewHolder to make sure you don't display information from the previous item. You will get rid of the problem if your onBindViewHolder always returns correct representation of a view for the given position. If you forget to clean up some reused views, you might see duplicated information while you scroll. If this is not really clear, please read how the ViewHolder pattern works, it's pretty simple once you get used to it.
Good luck!
I would like to set the Height of my ScrollView per Code dynamically cause my ScrollView is actually higher then it shall be (empty space at bottom).
My thoughts were, that I could get the Heights of all Controls within the the ScrollView, sum-up them and then I could set that Height to my ScrollView.
What I tried is following code:
protected override void OnStart()
{
base.OnStart();
SetScrollViewSize();
}
private void SetScrollViewSize()
{
var root = FindViewById<ScrollView>(Resource.Id.root);
if (root != null)
{
var controls = GetSelfAndChildrenRecursive(root); //Gives me all Controls in the root (The ScrollView)
int heightOfAllControlsTogether = 0;
foreach (ViewGroup control in controls)
{
heightOfAllControlsTogether += control.Height;
}
ViewGroup.LayoutParams parameters = new ViewGroup.LayoutParams(root.Width, heightOfAllControlsTogether);
root.LayoutParameters = parameters;
}
}
The Heights and MeasuredHeights are always 0 (zero) - (I know it needs to be rendered first, but what would be the right place then?) and I'm not even sure if my approach would work.
Any Help would be appreciated!
Well I found out why.
The Background-Graphic was too high. That was creating the empty space at the bottom.
THis is an example to set scrollview height based on total height of Children:
private void setScrollViewHeightBasedOnChildren() {
int size = layoutRoot.getChildCount();
LinearLayout item = (LinearLayout) layoutRoot.getChildAt(0);
item.measure(0, 0);
int height = item.getMeasuredHeight();
//Reset size of ScrollView
scroll.setMinimumHeight(height / size);
scroll.invalidate();
}
See details: Set the height of ScrollView equal total height of children