I have been spending hours unsuccessfully trying to adjust the width, height, and offset of a simple view in Android as the result of a button press. I have discovered that setTranslationX and setTranslationY always work; the legacy method of setLayoutParams never works once the view is laid out initially. Calls to requestLayout() and invalidate() similarly produce no results.
I have tried to setLayoutParams within the context of posting a runnable, but this does nothing.
Because setTranslationX always works, I would just use that, but unfortunately there is no equivalent method like setWidth or setHeight.
As you can see in the AOSP, setTranslationX makes a call to invalidateViewProperty, which is a private method of View.
Is there an equivalent method to setTranslationX to adjust a view width or view margin, that presumably triggers invalidateViewProperty, and, by extension, works reliably?
EDIT
While in some situations, setLayoutParams may be expected to work after the initial layout, I am in a situation where setLayoutParams has no effect after the initial layout, but setTranslationX does. My setup is as follows:
Running Android KitKat 4.4
The view in question is MATCH_PARENT for both width, height
The view in question is a child of a RelativeLayout
The view in question is a View class with a simple solid-color background drawable
Here is the view:
<View
android:id="#+id/border"
android:background="#drawable/match_background_border_transparent"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
And here is the (non-working) code meant to dynamically alter its margins, but has no effect. Again, if I call setTranslationX, that always works.
holder.toggleButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
imageBorder.post(new Runnable() {
#Override
public void run() {
RelativeLayout.LayoutParams p = (LayoutParams) imageBorder.getLayoutParams();
p.leftMargin = 20;
p.rightMargin = 20;
p.topMargin = 20;
p.bottomMargin = 20;
imageBorder.setLayoutParams(p);
// imageBorder.setTranslationX does have an effect if I included it here
}
});
}
});
I have determined why setTranslationX was working, but setLayoutParams was not. My views were ultimately descendents of an AdapterView. I was able to programmatically manipulate LayoutParams of the AdapterView and his siblings, but none of the AdapterView's descendents.
Additional research showed that this was a common Android question:
Margin on ListView items in android
Why LinearLayout's margin is being ignored if used as ListView row view
What was confusing was that this view was several levels deep; i.e., it went:
AdapterView -> FrameLayout -> RelativeLayout -> View
Anyhow, I was able to accomplish my programmatic layout goals by wrapping view in another view, and using setPadding.
Related
What I intend to achieve
The item view should occupy the entire height of the item
It could be that the item height is lesser than the height of the tallest item in the recyclerview, in which case it should just stick to the top like in the screenshot above.
The bug I'm running into
As in the screenshot above, views are getting truncated.
What I've tried so far
Initially I went with wrap_content on the recyclerview, now that it is supported. It didn't work when none of the views visible on the screen at the time were the tallest. This makes sense in how the view hierarchy is laid out. How can the height of something which hasn't even been bound to any data yet be calculated if the height is dependent on that data?
Workaround time :S
Instead of trying a custom layoutmanager, I first went with what I felt needed to be done - laying out all item views at the beginning to figure out their height.
There's a progressbar and an animation playing in the upper part of the screen to catch the user's attention while all this happens with recyclerview visibility set to invisible. I use two things, one didn't suffice - I've attached an observer in the adapter's onViewAttached() call and I've used a scroll change listener as well. There's a LinearSnapHelper attached to the recycler view to snap to adjacent (next or previous, depending on the scroll direction) position on scroll.
In this setup,
I'm going to each position in the recyclerview using layoutManager.smoothScrollToPosition()
Getting the child view height using
View currentChildView = binding.nextRv.getChildAt(layoutManager.findFirstCompletelyVisibleItemPosition());
if (currentChildView != null) {
currentChildHeight = currentChildView.getHeight();
}
in scroll change listener on RecyclerView.SCROLL_STATE_IDLE or by passing the height to the view attached observer mentioned above in the adapter's onViewAttachedToWindow()
#Override
public void onViewAttachedToWindow(BindingViewHolder holder) {
if (mObserver != null) {
mObserver.onViewAttached(holder.binding.getRoot().getHeight());
}
}
Storing a maxHeight that changes to the max of maxHeight and new child's height.
As is evident, this is ugly. Plus it doesn't give me the current view's height - onAttached means it's only just attached, not measured and laid out. It is the recycled view, not the view bound to current data item. Which presents problems like the truncation of view illustrated above.
I've also tried wrap_content height on the recycler view and invalidating from recycler's parent till the recycler and the child on scroll coming to SCROLL_STATE_IDLE. Doesn't work.
I'm not sure how a custom layoutmanager can help here.
Can someone guide me in the right direction?
I could not accept #Pradeep Kumar Kushwaha's answer because against one solution, I do not want different font sizes in the list. Consistency is a key element in design. Second alternative he gave couldn't work because with ellipsize I would need to give a "more" button of some sort for user to read the entire content and my text view is already taking a click action. Putting more some place else would again not be good design.
Changing the design with the simple compromise of resizing the recyclerview when the tallest, truncated item comes into focus, it turns into the simple use case of notifyItemChanged(). Even for the attempt I made using the view attached observer and scroll state listener, notifyItemChanged could be used but that approach is just too hacky. This I can live with in both code and design. Here goes the code required.
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
int position = ((LinearLayoutManager) binding.nextRv.getLayoutManager())
.findFirstVisibleItemPosition();
if (position != nextSnippetAdapter.getItemCount() - 1) {
binding.nextRv.getAdapter().notifyItemRangeChanged(position, 2);
} else {
binding.nextRv.getAdapter().notifyItemChanged(position);
}
}
}
For my particular setup, calling for just these two elements works. It can further be optimized so as to call for single element at position + 1 in most cases, and checking and calling for the appropriate one in corner (literal) cases.
Inside your adapter where I can find two cards one on top and another on bottom
How I would have defined my layout is like this:
Cardview1
LinearLayout1 --> orientation vertical
cardview2 (Top card where text is written)
Linearlayout2 (where I can see icons such as like etc)-->orientation horizontal
Now fix the height of Linearlayout2 by setting it to wrap content.
And the height of cardview2 should be 0dp and add weight = 1
Now inside cardview2 add a TextView1 to matchparent in height and width.
Better inside textview1 add ellipsize to end and add max lines
If you want to show all lines try to find autoresizetextview library it can be founded here --> AutoResizeTextView
Hope it helps.
I think the recyclerview can be set to height wrap_content. And the items can be make like height to match_parent.
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layput_height="wrap_content"/>
Item as:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
// your coode
</androidx.constraintlayout.widget.ConstraintLayout>
I had little more requirement than the question. Even my problem solved in the way.
Remember I am using:
androidx.recyclerview:recyclerview:1.0.0-beta01
dependency for the project
I have a custom view added as a child of a LinearLayout. I need this custom view to anchor to the bottom irrespective of the parent viewgroup. Here's how I'm doing it.
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!drawn) {
drawn = true;
Rect rect = new Rect();
((View) getParent()).getGlobalVisibleRect(rect);
setY(rect.bottom - rect.top - getContext().getResources().getDimension(R.dimen.bottom_bar_height));
setZ(UiUtils.dpToPx(8f));
forceLayout();
bringToFront();
} else {
binding.bottomBarLl.draw(canvas);
}
}
When I add the custom view to a LinearLayout parent, if the children above this child have match_parent height, this one doesn't show.
Below is a screenshot from the layout inspector where it is shown to be laid out but doesn't show.
The inspector also shows that the getZ() of the custom view is 24.
I'm building for API level 21+ but I've also tried ((View) getParent()).invalidate(); and getParent().requestLayout(); in the onDraw() to no good.
From the documentation for View.bringToFront()
bringToFront
Added in API level 1
void bringToFront ()
Change the view's z order in
the tree, so it's on top of other sibling views. This ordering change
may affect layout, if the parent container uses an order-dependent
layout scheme (e.g., LinearLayout). Prior to KITKAT this method should
be followed by calls to requestLayout() and invalidate() on the view's
parent to force the parent to redraw with the new child ordering.
this should be working, but it isn't for me. What am I doing incorrectly?
Like other questions which couldn't get it to work with a LinearLayout, I also used a coordinator layout finally where the linear layout and my custom view are siblings and things are working fine. It would be great if someone could explain the documentation though, since it is misleading otherwise.
I have this issue where I have a relative layout that has two child relative layouts (leftpanel and rightpanel). They're inside a custom layout for listview items and each item is updated from a json response from the server. So the size depends on what the server provides.
Issue: I want to have each panel's height to match each other, but it seems that setting layout_height to match_parent doesn't work (actually, if this can be resolved, then no more problems).
What I did: I programmatically set the align top and bottom of each panel to each other -- if the other's bigger, adjust the other one and vice versa. So what I did was to have a view (rightpanel) to listen to rightPanel.getViewTreeObserver().addOnScrollChangedListener(), and call the method below everytime there's a scroll change:
private void updateLayoutAlignmentParams(ViewHolder viewHolder) {
RelativeLayout.LayoutParams leftPanelLayoutParams = (RelativeLayout.LayoutParams)viewHolder.leftPanel.getLayoutParams();
RelativeLayout.LayoutParams rightPanelLayoutParams = (RelativeLayout.LayoutParams)viewHolder.rightPanel.getLayoutParams();
int leftPanelHeight = viewHolder.leftPanel.getHeight();
int rightPanelHeight = viewHolder.rightPanel.getHeight();
if(leftPanelHeight > rightPanelHeight) {
rightPanelLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, 0);
rightPanelLayoutParams.addRule(RelativeLayout.ALIGN_TOP, 0);
leftPanelLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, viewHolder.rightPanel.getId());
leftPanelLayoutParams.addRule(RelativeLayout.ALIGN_TOP, viewHolder.rightPanel.getId());
} else {
leftPanelLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, 0);
leftPanelLayoutParams.addRule(RelativeLayout.ALIGN_TOP, 0);
rightPanelLayoutParams.addRule(RelativeLayout.ALIGN_BOTTOM, viewHolder.leftPanel.getId());
rightPanelLayoutParams.addRule(RelativeLayout.ALIGN_TOP, viewHolder.leftPanel.getId());
}
}
What happens: not all the views get updated while scrolling; so I get a lop-sided listview item where one is bigger than the other vertically but some do adjust well. Odd thing is, when the item gets out of view, it's lop-sided, then gets corrected consistently.
Note: I also tried
addOnDrawListener() - every item is updated but I get an ArrayList out of bounds index but doesn't point to any line in my code. Wouldn't be the best solution anyway as I need to support devices with API < 16.
setOnGlobalLayoutListener() - Nothing happens.
Please let me know if you know why or have a better solution.
Finally [kindof] fixed it! Replaced the whole method with the code below:
private void updateLayoutAlignmentParams(ViewHolder viewHolder) {
viewHolder.rightPanel.setMinimumHeight(viewHolder.leftPanel.getHeight());
viewHolder.leftPanel.setMinimumHeight(viewHolder.rightPanel.getHeight());
}
Although, I was able to achieve having the left and right panel aligned with each other using the code above. I'm now having issues where the previous view's height and width are retained when I switch views. :(
Edit:
Okay, I ended up using LinearLayout to wrap the whole listview item. Not really sure why RelativeLayout isn't complying with match_parent, though.
I am trying to create a view that slides up from the bottom of the screen. I tried setting the initial position of the view (which should be offscreen) in xml, but instead of placing the imageview where I specified, it truncated it. My second thought was to set the position of the view programatically inside the onWindowFocusChanged method. Here's my code
#Override
public void onWindowFocusChanged(boolean hasFocus) {
if(hasFocus) {
slide_dock.layout(slide_dock.getLeft(), phone_height - 70, slide_dock.getRight(), phone_height + 230);
}
}
The problem is that this only works SOME of the time. I've been debugging it, and I believe the issue is that the layout values of slide_dock get altered after my onWindowFocusChanged function completes, I'm just not sure where. Can anyone here help me out? Or link me to somewhere that explains the layout process in-depth? I've been searching around to no avail.
Have you tried using the Animation framework? Use a RelativeLayout and align your child view to the bottom of the parent. Then use the following animation, maybe showing and hiding your view appropriately with View.setVisibility(int)
View myView = View(this);
TranslateAnimation slideUp = new TranslateAnimation(myView.getHeight(), 0, 0, 0);
slideUp.setDuration(250); // millis
slideUp.setFillAfter(true); // Required for animation to "stick" when done
myView.startAnimation(slideUp);
You might have to play with the TranslateAnimation constructor parameters to get it to work right (this is from the top of my head).
So I figured out the cause for the issue above, and I'm posting it here in case anyone ever runs into the same problem.
The reason why the ImageView was resizing was because in ImageView's onMeasure function, it resizes itself if it doesn't think that it will fit onto the screen. You can view the ImageView source here and see how it works: http://www.google.com/codesearch/p?hl=en#uX1GffpyOZk/core/java/android/widget/ImageView.java&d=3
To work around this, I created a custom view that extended ImageView and overrode the onMeasure method. In my new onMeasure method, I simply called setDimension to give my new view the dimensions that I wanted it to have, this effectively stopped the view from resizing as it was doing earlier
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(measureWidth(widthMeasureSpec), 300);
}
There is apparently a bug in Android which breaks View.bringToFront.
If I have Views arranged in a GridView and want to bring a View to front by calling bringToFront(), it may get moved to the bottom right position in the GridView.
Something shady is going on there, so I can't use bringToFront(). When I don't call it, everything works fine. But the Views overlap - if I use scale animation to scale up a View, it's partially obscured by the Views to the bottom and to the right.
I've checked out bringToFront's source and it calls parent.bringChildToFront(...)
it's this method
public void bringChildToFront(View child) {
int index = indexOfChild(child);
if (index >= 0) {
removeFromArray(index);
addInArray(child, mChildrenCount);
child.mParent = this;
}
}
it apparently removes the View from itself! Without mercy! Is a ViewGroup so dumb that it can't manage Z-indexes in any other way that shuffling controls around? Obviously when GridView removes its child and then adds it back again, it gets placed at the end of the Grid!
Is there anything I can do to show a View on top of others without resorting to some hacking? One thing that comes to my mind is to create another View above the GridView, which will appear to be above the one I'm trying to bringToFront(), but I don't like this solution.
apparently I missed the android:clipChildren="false" property in GridView. It solves my problem.
Have you tried making the View focusable and just calling requestFocus()?
Calling bringToFront() changes the ordering in the GridView. Try creating an ImageView with an image of the view you want to animate and animate that instead.
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
final ImageView imageView = new ImageView(getActivity());
imageView.setImageBitmap(bitmap);
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(view.getWidth() , view.getHeight());
imageView.setLayoutParams(params);
rootview.addView(imageView);
Add an animation listener to your animation and remove the ImageView at the end of the animation.
From API 21 you can call:
view.setZ(float)
If you target API above 21, you can simply add the attribute android:translationZ="xxx dp" to your XML.
Please note that if you add elevation in views like cardview, you go into the Z axis. And if you want a view come to foreground using this way, you just have to make it higher than the elevation you set.
Example : 6 dp elevation in cardview will require a 7dp in the attribute translationZ of the view you want foreground.