Android custom layout wrap content - android

I am implementing custom layout.
I put ImageViews to my layout and in onMeasure() method call all children child.measure(w, h)
Everything works fine instead of wrap_content behavior.
Views behave like they are match_parent and are full screen size instead of wrap content.
When I want to make view to be wrap_content doing this
if (boxSize == MyBox.RATIO_MEASURE) {
spec = MeasureSpec.AT_MOST;
size = containerSize;
code:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
// custom layout logic
MyBox box = getBox(v);
int width = getMeasuredSpec(box.getWidth(), widthMeasureSpec);
int height = getMeasuredSpec(box.getHeight(), heightMeasureSpec);
v.measure(width, height);
}
}
private int getMeasuredSpec(float boxSize, int containerSpec) {
int containerSize = MeasureSpec.getSize(containerSpec);
int spec;
int size;
if (boxSize == MyBox.RATIO_MEASURE) {
spec = MeasureSpec.AT_MOST;
size = containerSize;
} else if (boxSize == 1) {
spec = MeasureSpec.AT_MOST;
size = containerSize;
} else {
spec = MeasureSpec.EXACTLY;
size = (int) (MyBox.isScreenDependent(boxSize)
? containerSize * boxSize
: boxSize);
}
return MeasureSpec.makeMeasureSpec(size, spec);
}

Hrm.... this seems tricky b/c you are doing it from inside Java.
I think the magical class that you want is LayoutParams
https://developer.android.com/reference/android/view/ViewGroup.LayoutParams.html#MATCH_PARENT
int WRAP_CONTENT= -2;
LayoutParams params= new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
ImageView bNext_Word = (ImageView) rootView.findViewById(R.id.myimage);
mImage.setLayoutParams( params);

Related

onMeasure call to getMeasuredWidth or Height return 0 for children

I'm trying to dynamically add items to the my CustomView and make the final height the size of all the children added together. The only problem is the child.getMeasuredHeight or child.getMeasuredWidth always returns a value of 0 during run time. When I'm debugging it will randomly 1 out of 10 times seem to actually contain the data I was actually expecting with a value of 192. If I also hard code a value into the layout parameter instead of using WRAP_CONTENT it still shows a value of 0. Is there something that I'm doing wrong.
xml file
<CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_marginTop="80dp"
android:orientation="vertical"
android:layout_width="200dp"
android:layout_height="100dp"
android:id="#+id/custom_layout"
android:background="#drawable/custom_shape"/>
</CustomLayout>
Here's a part of my CustomLayout.java class
public class CustomLayout extends LinearLayout {
public CustomLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomLayout(Context context) {
super(context);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
int maxHeight = 0;
int maxWidth = 0;
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
for(int i=0; i <count; i++) {
final View child = getChildAt(i);
if(child.getVisibility() != GONE) {
maxHeight += child.getMeasuredHeight();
}
}
setMeasuredDimension(widthSize,maxHeight);
}
In a part of my main activity
Button b = new Button(this);
b.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
b.setText("Sample Test");
b.setTextSize(14);
mCustomView.addView(b);
You will need to ask each child of the ViewGroup to measure itself before you can access its dimensions. This is done with a call to [measureChild](https://developer.android.coma/reference/android/view/ViewGroup.html#measureChild(android.view.View, int, int)) or [measureChildWithMargins](https://developer.android.coma/reference/android/view/ViewGroup.html#measureChild(android.view.View, int, int)).
Take a look at the developer guide for ViewGroup to see how the child measurements are obtained.
// Iterate through all children, measuring them and computing our dimensions
// from their size.
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
// Measure the child.
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// Update our size information based on the layout params. Children
// that asked to be positioned on the left or right go in those gutters.
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.position == LayoutParams.POSITION_LEFT) {
mLeftWidth += Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
} else if (lp.position == LayoutParams.POSITION_RIGHT) {
mRightWidth += Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
} else {
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
}
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
}
}

Correctly layout children with onLayout in Custom ViewGroup

I am creating a custom ViewGroup. I does need to have 2 FrameLayout one above the other; the one that stays to the bottom must be 20dp, while the other one must cover the rest of the view.
onMeasure
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(widthSize, heightSize);
final View content = getChildAt(CONTENT_INDEX);
final View bar = getChildAt(BAR_INDEX);
content.measure(
MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(heightSize - getPixels(BAR_HEIGHT), MeasureSpec.EXACTLY)
);
bar.measure(
MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getPixels(BAR_HEIGHT), MeasureSpec.EXACTLY)
);
onLayout
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mInLayout = true;
final View content = getChildAt(CONTENT_INDEX);
final View bar = getChildAt(BAR_INDEX);
if (content.getVisibility() != GONE) {
content.layout(0, 0, content.getMeasuredWidth(), content.getMeasuredHeight());
}
if (bar.getVisibility() != GONE) {
bar.layout(0, content.getMeasuredHeight(), bar.getMeasuredWidth(), 0);
}
mInLayout = false;
mFirstLayout = false;
}
}
The views I am adding to this custom ViewGroup
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
mContentContainer = new FrameLayout(getContext());
mContentContainer.setLayoutParams(lp);
mBarContainer = new FrameLayout(getContext());
mBarContainer.setLayoutParams(lp);
// ... adding stuff to both containers ....
addView(mContentContainer, 0);
addView(mBarContainer, 1);
The issue
mContentContainer gets rendered correctly (from top=0 to bottom=(totalHeight - bar height) and match parent for the width), while the bar is not rendered.
The last parameter in the View#layout() method is the bottom of the View. For your bar, you're passing 0, but it should be the height of your custom View, which you can figure from the t and b values passed into onLayout().
bar.layout(0, content.getMeasuredHeight(), bar.getMeasuredWidth(), b - t);

extended View Group forces the child dimensions

I wrote the following ViewGroup
public class myViewGroup extends ViewGroup{
List<View> qResult;
List<Point> qLoc;
ImageView qImage;
public QueryViewLayout(Context context){
super(context);
}
public QueryViewLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public QueryViewLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
qResult = new LinkedList<View>();
qLoc = new LinkedList<Point>();
qImage = null;
}
public void addMainView(ImageBorderView view){
qImage = view;
super.removeAllViews();
super.addView(view);
}
public void addResultView(View result, Point loc){
super.addView(result);
qResult.add(result);
qLoc.add(loc);
}
/**
* Any layout manager that doesn't scroll will want this.
*/
#Override
public boolean shouldDelayChildPressedState() {
return false;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
// Measurement will ultimately be computing these values.
int maxHeight = 0;
int maxWidth = 0;
int childState = 0;
// Only main view affects the layouts measure
if (qImage != null) {
if (qImage.getVisibility() != GONE) {
// Measure the child.
qImage.measure(widthMeasureSpec, heightMeasureSpec);
maxWidth = qImage.getMeasuredWidth();
maxHeight = qImage.getMeasuredHeight();
childState = qImage.getMeasuredState();
}
}
for (View child:qResult){
if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED)
child.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST));
}
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
// Report our final dimensions.
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int count = getChildCount();
int parentLeft = left + getPaddingLeft();
int parentRight = right - getPaddingRight();
final int parentTop = top + getPaddingTop();
final int parentBottom = bottom - getPaddingBottom();
if (qImage == null) return;
qImage.layout(parentLeft, parentTop, parentRight, parentBottom);
Iterator<Point> loc = qLoc.iterator();
for (View child:qResult) {
Point p = loc.next();
if (child.getVisibility() != GONE) {
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
Point locOnView = qImage.projectOnView(p);
width = (width < (int) Math.max(parentRight - (int) locOnView.x, locOnView.x - parentLeft)) ?
width : (parentLeft + parentRight)/2;
height = (height < (int) Math.max(parentBottom - (int) locOnView.y, locOnView.y - parentTop)) ?
height : (parentBottom + parentTop)/2;
int x = (width < (parentRight - (int) locOnView.x)) ? (int) locOnView.x : (parentRight - width);
int y = (height < parentBottom - (int) locOnView.y) ? (int) locOnView.y : (parentBottom - height);
// Place the child.
child.layout(x, y, x + width, y + height);
}
}
}
}
It is supposed to show some arbitrary view on top of an image, given a location for that view, when I use a GridView as the arbitrary view, even though I have defined a certain width for the GridView it is forced to have a width as large as the frame. In the measure phase I changed the mode to
MeasureSpec.AT_MOST
for both width and height of the overlay view, but this does not seem to work, can someone please help.
here is the xml where I, inflate the GridView from
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/result_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:columnWidth="#dimen/result_view_column_width"
android:numColumns="2"
android:verticalSpacing="2dp"
android:horizontalSpacing="2dp"
android:stretchMode="none"
android:gravity="center"
android:layout_margin = "2dp"
android:background="#drawable/solid_with_shadow" />
After a lot of trial and error, replacing
child.measure(MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST));
with
measureChild(child, MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST));
worked for me, I am not sure why, but a wild guess would be calling measure on a child does not read all the xml props, but measureChild(child, ...) does.

RecyclerView GridLayoutManager: how to auto-detect span count?

Using the new GridLayoutManager: https://developer.android.com/reference/android/support/v7/widget/GridLayoutManager.html
It takes an explicit span count, so the problem now becomes: how do you know how many "spans" fit per row? This is a grid, after all. There should be as many spans as the RecyclerView can fit, based on measured width.
Using the old GridView, you would just set the "columnWidth" property and it would automatically detect how many columns fit. This is basically what I want to replicate for the RecyclerView:
add OnLayoutChangeListener on the RecyclerView
in this callback, inflate a single 'grid item' and measure it
spanCount = recyclerViewWidth / singleItemWidth;
This seems like pretty common behavior, so is there a simpler way that I'm not seeing?
Personaly I don't like to subclass RecyclerView for this, because for me it seems that there is GridLayoutManager's responsibility to detect span count. So after some android source code digging for RecyclerView and GridLayoutManager I wrote my own class extended GridLayoutManager that do the job:
public class GridAutofitLayoutManager extends GridLayoutManager
{
private int columnWidth;
private boolean isColumnWidthChanged = true;
private int lastWidth;
private int lastHeight;
public GridAutofitLayoutManager(#NonNull final Context context, final int columnWidth) {
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1);
setColumnWidth(checkedColumnWidth(context, columnWidth));
}
public GridAutofitLayoutManager(
#NonNull final Context context,
final int columnWidth,
final int orientation,
final boolean reverseLayout) {
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1, orientation, reverseLayout);
setColumnWidth(checkedColumnWidth(context, columnWidth));
}
private int checkedColumnWidth(#NonNull final Context context, final int columnWidth) {
if (columnWidth <= 0) {
/* Set default columnWidth value (48dp here). It is better to move this constant
to static constant on top, but we need context to convert it to dp, so can't really
do so. */
columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
context.getResources().getDisplayMetrics());
}
return columnWidth;
}
public void setColumnWidth(final int newColumnWidth) {
if (newColumnWidth > 0 && newColumnWidth != columnWidth) {
columnWidth = newColumnWidth;
isColumnWidthChanged = true;
}
}
#Override
public void onLayoutChildren(#NonNull final RecyclerView.Recycler recycler, #NonNull final RecyclerView.State state) {
final int width = getWidth();
final int height = getHeight();
if (columnWidth > 0 && width > 0 && height > 0 && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
final int totalSpace;
if (getOrientation() == VERTICAL) {
totalSpace = width - getPaddingRight() - getPaddingLeft();
} else {
totalSpace = height - getPaddingTop() - getPaddingBottom();
}
final int spanCount = Math.max(1, totalSpace / columnWidth);
setSpanCount(spanCount);
isColumnWidthChanged = false;
}
lastWidth = width;
lastHeight = height;
super.onLayoutChildren(recycler, state);
}
}
I don't actually remember why I choosed to set span count in onLayoutChildren, I wrote this class some time ago. But the point is we need to do so after view get measured. so we can get it's height and width.
EDIT 1: Fix error in code caused to incorrectly setting span count. Thanks user #Elyees Abouda for reporting and suggesting solution.
EDIT 2: Some small refactoring and fix edge case with manual orientation changes handling. Thanks user #tatarize for reporting and suggesting solution.
I accomplished this using a ViewTreeObserver to get the width of the RecylcerView once rendered and then getting the fixed dimensions of my CardView from resources and then setting the span count after doing my calculations. It is only really applicable if the items you are displaying are of a fixed width. This helped me automatically populate the grid regardless of screen size or orientation.
mRecyclerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
#Override
public void onGlobalLayout() {
mRecyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int viewWidth = mRecyclerView.getMeasuredWidth();
float cardViewWidth = getActivity().getResources().getDimension(R.dimen.cardview_layout_width);
int newSpanCount = (int) Math.floor(viewWidth / cardViewWidth);
mLayoutManager.setSpanCount(newSpanCount);
mLayoutManager.requestLayout();
}
});
Well, this is what I used, fairly basic, but gets the job done for me. This code basically gets the screen width in dips and then divides by 300 (or whatever width you're using for your adapter's layout). So smaller phones with 300-500 dip width only display one column, tablets 2-3 columns etc. Simple, fuss free and without downside, as far as I can see.
Display display = getActivity().getWindowManager().getDefaultDisplay();
DisplayMetrics outMetrics = new DisplayMetrics();
display.getMetrics(outMetrics);
float density = getResources().getDisplayMetrics().density;
float dpWidth = outMetrics.widthPixels / density;
int columns = Math.round(dpWidth/300);
mLayoutManager = new GridLayoutManager(getActivity(),columns);
mRecyclerView.setLayoutManager(mLayoutManager);
I extended the RecyclerView and overrode the onMeasure method.
I set an item width(member variable) as early as I can,with a default of 1. This also updates on configuration changed. This will now have as many rows as can fit in portrait,landscape,phone/tablet etc.
#Override
protected void onMeasure(int widthSpec, int heightSpec) {
super.onMeasure(widthSpec, heightSpec);
int width = MeasureSpec.getSize(widthSpec);
if(width != 0){
int spans = width / mItemWidth;
if(spans > 0){
mLayoutManager.setSpanCount(spans);
}
}
}
A better way (imo) would be to define different span counts in (many) different values directories and let the device automatically select which span count to use. For example:
values/integers.xml -> span_count=3
values-w480dp/integers.xml -> span_count=4
values-w600dp/integers.xml -> span_count=5
I'm posting this just in case someone gets weird column width as in my case.
I'm not able to comment on #s-marks's answer due to my low reputation. I applied his solution solution but I got some weird column width, so I modified checkedColumnWidth function as follows:
private int checkedColumnWidth(Context context, int columnWidth)
{
if (columnWidth <= 0)
{
/* Set default columnWidth value (48dp here). It is better to move this constant
to static constant on top, but we need context to convert it to dp, so can't really
do so. */
columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
context.getResources().getDisplayMetrics());
}
else
{
columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, columnWidth,
context.getResources().getDisplayMetrics());
}
return columnWidth;
}
By converting the given column width into DP fixed the issue.
I conclusion above answers here
To accommodate orientation change on s-marks's answer, I added a check on width change (width from getWidth(), not column width).
private boolean mWidthChanged = true;
private int mWidth;
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
{
int width = getWidth();
int height = getHeight();
if (width != mWidth) {
mWidthChanged = true;
mWidth = width;
}
if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0
|| mWidthChanged)
{
int totalSpace;
if (getOrientation() == VERTICAL)
{
totalSpace = width - getPaddingRight() - getPaddingLeft();
}
else
{
totalSpace = height - getPaddingTop() - getPaddingBottom();
}
int spanCount = Math.max(1, totalSpace / mColumnWidth);
setSpanCount(spanCount);
mColumnWidthChanged = false;
mWidthChanged = false;
}
super.onLayoutChildren(recycler, state);
}
The upvoted solution is fine, but handles the incoming values as pixels, which can trip you up if you're hardcoding values for testing and assuming dp. Easiest way is probably to put the column width in a dimension and read it when configuring the GridAutofitLayoutManager, which will automatically convert dp to correct pixel value:
new GridAutofitLayoutManager(getActivity(), (int)getActivity().getResources().getDimension(R.dimen.card_width))
Set minimal fixed width of imageView (144dp x 144dp for example)
When you create GridLayoutManager, you need to know how much columns will be with minimal size of imageView:
WindowManager wm = (WindowManager) this.getSystemService(Context.WINDOW_SERVICE); //Получаем размер экрана
Display display = wm.getDefaultDisplay();
Point point = new Point();
display.getSize(point);
int screenWidth = point.x; //Ширина экрана
int photoWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 144, this.getResources().getDisplayMetrics()); //Переводим в точки
int columnsCount = screenWidth/photoWidth; //Число столбцов
GridLayoutManager gridLayoutManager = new GridLayoutManager(this, columnsCount);
recyclerView.setLayoutManager(gridLayoutManager);
After that you need to resize imageView in adapter if you have space in column. You may send newImageViewSize then inisilize adapter from activity there you calculate screen and column count:
#Override //Заполнение нашей плитки
public void onBindViewHolder(PhotoHolder holder, int position) {
...
ViewGroup.LayoutParams photoParams = holder.photo.getLayoutParams(); //Параметры нашей фотографии
int newImageViewSize = screenWidth/columnsCount; //Новый размер фотографии
photoParams.width = newImageViewSize; //Установка нового размера
photoParams.height = newImageViewSize;
holder.photo.setLayoutParams(photoParams); //Установка параметров
...
}
It works in both orientations. In vertical I have 2 columns and in horizontal - 4 columns. The result: https://i.stack.imgur.com/WHvyD.jpg
This is s.maks' class with a minor fix for when the recyclerview itself changes size. Such as when you deal with the orientation changes yourself (in the manifest android:configChanges="orientation|screenSize|keyboardHidden"), or some other reason the recyclerview might change size without the mColumnWidth changing. I also changed the int value it takes to be the resource of the size and allowed a constructor of no resource then setColumnWidth to do that yourself.
public class GridAutofitLayoutManager extends GridLayoutManager {
private Context context;
private float mColumnWidth;
private float currentColumnWidth = -1;
private int currentWidth = -1;
private int currentHeight = -1;
public GridAutofitLayoutManager(Context context) {
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1);
this.context = context;
setColumnWidthByResource(-1);
}
public GridAutofitLayoutManager(Context context, int resource) {
this(context);
this.context = context;
setColumnWidthByResource(resource);
}
public GridAutofitLayoutManager(Context context, int resource, int orientation, boolean reverseLayout) {
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1, orientation, reverseLayout);
this.context = context;
setColumnWidthByResource(resource);
}
public void setColumnWidthByResource(int resource) {
if (resource >= 0) {
mColumnWidth = context.getResources().getDimension(resource);
} else {
/* Set default columnWidth value (48dp here). It is better to move this constant
to static constant on top, but we need context to convert it to dp, so can't really
do so. */
mColumnWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
context.getResources().getDisplayMetrics());
}
}
public void setColumnWidth(float newColumnWidth) {
mColumnWidth = newColumnWidth;
}
#Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
recalculateSpanCount();
super.onLayoutChildren(recycler, state);
}
public void recalculateSpanCount() {
int width = getWidth();
if (width <= 0) return;
int height = getHeight();
if (height <= 0) return;
if (mColumnWidth <= 0) return;
if ((width != currentWidth) || (height != currentHeight) || (mColumnWidth != currentColumnWidth)) {
int totalSpace;
if (getOrientation() == VERTICAL) {
totalSpace = width - getPaddingRight() - getPaddingLeft();
} else {
totalSpace = height - getPaddingTop() - getPaddingBottom();
}
int spanCount = (int) Math.max(1, Math.floor(totalSpace / mColumnWidth));
setSpanCount(spanCount);
currentColumnWidth = mColumnWidth;
currentWidth = width;
currentHeight = height;
}
}
}
I like s.maks' answer but I found another edge case: If you set the height of the RecyclerView to WRAP_CONTENT it may happen that the height of the recyclerview is calculated incorrectly based on an outdated spanCount value. The solution I found is a small modification of the proposed onLayoutChildren() method:
public void onLayoutChildren(#NonNull final RecyclerView.Recycler recycler, #NonNull final RecyclerView.State state) {
final int width = getWidth();
final int height = getHeight();
if (columnWidth > 0 && (width > 0 || getOrientation() == HORIZONTAL) && (height > 0 || getOrientation() == VERTICAL) && (isColumnWidthChanged || lastWidth != width || lastHeight != height)) {
final int totalSpace;
if (getOrientation() == VERTICAL) {
totalSpace = width - getPaddingRight() - getPaddingLeft();
} else {
totalSpace = height - getPaddingTop() - getPaddingBottom();
}
final int spanCount = Math.max(1, totalSpace / columnWidth);
if (getSpanCount() != spanCount) {
setSpanCount(spanCount);
}
isColumnWidthChanged = false;
}
lastWidth = width;
lastHeight = height;
super.onLayoutChildren(recycler, state);
}
Set spanCount to a large number (which is the max number of column) and set a custom SpanSizeLookup to the GridLayoutManager.
mLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
#Override
public int getSpanSize(int i) {
return SPAN_COUNT / (int) (mRecyclerView.getMeasuredWidth()/ CELL_SIZE_IN_PX);
}
});
It's a bit ugly, but it work.
I think a manager like AutoSpanGridLayoutManager would be the best solution, but i didn't find anything like that.
EDIT : There is a bug, on some device it add blank space to the right
Here's the relevant parts of a wrapper I've been using to auto-detect the span count. You initialize it by calling setGridLayoutManager with a R.layout.my_grid_item reference, and it figures out how many of those can fit on each row.
public class AutoSpanRecyclerView extends RecyclerView {
private int m_gridMinSpans;
private int m_gridItemLayoutId;
private LayoutRequester m_layoutRequester = new LayoutRequester();
public void setGridLayoutManager( int orientation, int itemLayoutId, int minSpans ) {
GridLayoutManager layoutManager = new GridLayoutManager( getContext(), 2, orientation, false );
m_gridItemLayoutId = itemLayoutId;
m_gridMinSpans = minSpans;
setLayoutManager( layoutManager );
}
#Override
protected void onLayout( boolean changed, int left, int top, int right, int bottom ) {
super.onLayout( changed, left, top, right, bottom );
if( changed ) {
LayoutManager layoutManager = getLayoutManager();
if( layoutManager instanceof GridLayoutManager ) {
final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
LayoutInflater inflater = LayoutInflater.from( getContext() );
View item = inflater.inflate( m_gridItemLayoutId, this, false );
int measureSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED );
item.measure( measureSpec, measureSpec );
int itemWidth = item.getMeasuredWidth();
int recyclerViewWidth = getMeasuredWidth();
int spanCount = Math.max( m_gridMinSpans, recyclerViewWidth / itemWidth );
gridLayoutManager.setSpanCount( spanCount );
// if you call requestLayout() right here, you'll get ArrayIndexOutOfBoundsException when scrolling
post( m_layoutRequester );
}
}
}
private class LayoutRequester implements Runnable {
#Override
public void run() {
requestLayout();
}
}
}

How to Measure child expandable list height and width

I am using expandable list view to make 3 - level hierarchy, would like to know how to set internal list height and width.
I knew we have onMeasure for this purpose but in my case it not allowing me to capture whole space of parent list view.
may be I am giving wrong value to it, here is the code which is I am using for setting height and width of child expandable list.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
widthMeasureSpec = MeasureSpec.makeMeasureSpec(960,MeasureSpec.AT_MOST);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(800,MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
Currently it appearing as follow
<ParentGroup1 >
<ChildParentGroup>
<Child1>
<Child2>
<child3>
<ParentGroup2 >
and it should appear like below.
<ParentGroup1 >
<ChildParentGroup >
<Child1 >
<Child2 >
<child3 >
<ParentGroup2 >
Please advise/suggest for the same.
Thanks for your time.
Not sure if you're still looking for an answer, but this is how I did it: pass a reference to the parent view and a height measure (in this case, I used the size of the child list) in the constructor to create the child custom list.
public CustomExpandableList(Context context, View the_parentView, int the_heightMeasure)
{
super(context);
WIDTH = the_parentView!=null?the_parentView.getWidth():LayoutParams.MATCH_PARENT;
HEIGHT = the_heightMeasure * 500;
}
EDIT: Or to make the code more consistent, you could pass the width of the parentView and height measure to the constructor instead of passing the parent view itself.
CustomExpandableList(Context the_context, int the_width, int the_heightMeasure)
use this code to calculate expandable list view dynamically:
// calculate the height of expandable listView without expanded
private void setListViewHeight(ExpandableListView expListView) {
ListAdapter listAdapter = expListView.getAdapter();
int totalHeight = 0;
for (int i = 0; i < listAdapter.getCount(); i++) {
View listItem = listAdapter.getView(i, null, expListView);
listItem.measure(0, 0);
totalHeight += listItem.getMeasuredHeight();
System.out.println("i " + i);
}
ViewGroup.LayoutParams params = expListView.getLayoutParams();
params.height = totalHeight
+ (expListView.getDividerHeight() * (listAdapter.getCount() - 1));
System.out.println("params.height = " + params.height);
expListView.setLayoutParams(params);
expListView.requestLayout();
}
// calculate the height of expandable listview dynamically
private void setListViewHeight(ExpandableListView expListView, int group) {
ExpandableListAdapter listAdapter = expListView
.getExpandableListAdapter();
int totalHeight = 0;
int desiredWidth = MeasureSpec.makeMeasureSpec(expListView.getWidth(),
MeasureSpec.UNSPECIFIED);
for (int i = 0; i < listAdapter.getGroupCount(); i++) {
View groupItem = listAdapter.getGroupView(i, false, null,
expListView);
groupItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
totalHeight += groupItem.getMeasuredHeight();
if (((expListView.isGroupExpanded(i)) && (i == group))
|| ((!expListView.isGroupExpanded(i)) && (i == group))) {
for (int j = 0; j < listAdapter.getChildrenCount(i); j++) {
View listItem = listAdapter.getChildView(i, j, false, null,
expListView);
Log.e("Count", listAdapter.getChildrenCount(i) + "");
listItem.setLayoutParams(new ViewGroup.LayoutParams(
desiredWidth, MeasureSpec.UNSPECIFIED));
// listItem.measure(desiredWidth, MeasureSpec.UNSPECIFIED);
listItem.measure(MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED), MeasureSpec
.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
totalHeight += listItem.getMeasuredHeight();
System.out.println("totalHeight" + totalHeight);
Log.e("TEST", "gshdkfmjfy,");
}
}
}
ViewGroup.LayoutParams params = expListView.getLayoutParams();
int height = totalHeight
+ (expListView.getDividerHeight() * (listAdapter
.getGroupCount() - 1));
if (height < 10) {
height = 100;
}
params.height = height;
expListView.setLayoutParams(params);
expListView.requestLayout();
}
I succeeded in some days ago by doing this. It's a little bit more compact and without any additionnal parameter, and it works perfectly.
public static void setExpandableListViewHeightBasedOnChildren(ExpandableListView expandableListView){
ExpandableNotesListAdapter adapter = (ExpandableNotesListAdapter) expandableListView.getExpandableListAdapter();
if (adapter == null){
return;
}
int totalHeight = expandableListView.getPaddingTop() + expandableListView.getPaddingBottom();
for (int i = 0 ; i < adapter.getGroupCount() ; i++){
View groupItem = adapter.getGroupView(i, false, null, expandableListView);
groupItem.measure(View.MeasureSpec.UNSPECIFIED,View.MeasureSpec.UNSPECIFIED);
totalHeight += groupItem.getMeasuredHeight();
if (expandableListView.isGroupExpanded(i) ){
for( int j = 0 ; j < adapter.getChildrenCount(i) ; j++) {
View listItem = adapter.getChildView(i, j, false, null, expandableListView);
listItem.setLayoutParams(new LayoutParams(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED));
listItem.measure(View.MeasureSpec.makeMeasureSpec(0,
View.MeasureSpec.UNSPECIFIED), View.MeasureSpec
.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
totalHeight += listItem.getMeasuredHeight();
}
}
}
ViewGroup.LayoutParams params = expandableListView.getLayoutParams();
int height = totalHeight + expandableListView.getDividerHeight() * (adapter.getGroupCount() - 1);
if (height < 10)
height = 100;
params.height = height;
expandableListView.setLayoutParams(params);
expandableListView.requestLayout();
}
Don't forget to add this when you init your View, set your adapter, etc. :
Functions.setExpandableListViewHeightBasedOnChildren(listView);
listView.setOnGroupExpandListener(new ExpandableListView.OnGroupExpandListener() {
#Override
public void onGroupExpand(int groupPosition) {
Functions.setExpandableListViewHeightBasedOnChildren(listView);
}
});
Create one layout xml file for ParentGroup and ChildParentGroup , another layout xml file for Child. Now you are problem is reduced to two level hierarchy. Then In Expandable listview we have Parent view and childview methods to inflate and use the Parent and Child layouts. So in that mehods you can do whatever you want.
Simply remove the width code and it should work fine.
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
heightMeasureSpec = MeasureSpec.makeMeasureSpec(999999, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
I know its late, but if anyone has the same issue. You can solve it by creating a Custom ExpandableListView and using "MeasureSpec.EXACTLY":
public class CustomExpListView extends ExpandableListView{
public CustomExpListView(Context context){
super(context);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
widthMeasureSpec = MeasureSpec.makeMeasureSpec(960, MeasureSpec.EXACTLY);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(20000, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
Hope this helps to anyone. For me its working.
Adding to muhammadSalem's answer. This is how I solved my problem by calculating the height of expandableListView's children's total height.
private fun getSubItemTotalHeight(groupPosition: Int): Int {
val children: Int = mAdapter.getChildrenCount(groupPosition)
val desiredWidth: Int = View.MeasureSpec.makeMeasureSpec(mExpandableListView.width,
View.MeasureSpec.UNSPECIFIED)
var subItemTotalHeight = 0
repeat(children) {
val child = mAdapter.getChildView(groupPosition, it, true, null, null)
child.layoutParams = ViewGroup.LayoutParams(
desiredWidth, View.MeasureSpec.UNSPECIFIED)
child.measure(View.MeasureSpec.makeMeasureSpec(0,
View.MeasureSpec.UNSPECIFIED), View.MeasureSpec
.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))
subItemTotalHeight += child.measuredHeight
}
val dividerCount = children - 1
val dividerTotalCount = (dividerCount * mExpandableListView.dividerHeight).toFloat()
showToast(mExpandableListView.dividerHeight.toString())
val totalDividerPixels = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dividerTotalCount,
resources.displayMetrics
)
return subItemTotalHeight + totalDividerPixels.toInt()
}
One thing to note is that if you added a divider height for your expandableListview, you should include the calculations for it. What I did is convert the total divider height which is in dp into pixels and added it into the totalHeight. This solved the clipping issues I encountered.
Then to use it would be just :
mExpandableListView.setOnGroupExpandListener { groupPosition ->
mExpandableListView.layoutParams.height += getSubItemTotalHeight(groupPosition)
mExpandableListView.requestLayout()
}
mExpandableListView.setOnGroupCollapseListener { groupPosition ->
mExpandableListView.layoutParams.height -= getSubItemTotalHeight(groupPosition)
mExpandableListView.requestLayout()
}

Categories

Resources