I want to make a method that computes the margin values based on the parent's height and width. The code below outputs -1 for both height and width. How can I get the parent's height and width properly?
#RequiresApi(api = Build.VERSION_CODES.R)
public void setMarginsPercentages(Context context, #NotNull ConstraintLayout parent, #NotNull Object object, double leftPercentage, double topPercentage, double rightPercentage, double bottomPercentage) {
FrontEndObject item = getObjectType(object);
ConstraintLayout.LayoutParams params = new ConstraintLayout.LayoutParams(parent.getLayoutParams());
int height = params.height; //outputs -1
int width = params.width; //outputs -1
int left = (int) (leftPercentage * width);
int right = (int) (rightPercentage * width);
int top = (int) (topPercentage * height);
int bottom = (int) (bottomPercentage * height);
item.setMargins(parent, object, left, top, right, bottom);
}
There are couple problems here, the parms.height will give you this value (https://developer.android.com/reference/android/view/ViewGroup.LayoutParams#MATCH_PARENT), which is -1. Not the constraint layout's height in pixel you are looking for.
Also, You cannot use the width/height/getMeasuredWidth/getMeasuredHeight on a View before the system renders it (typically from onCreate/onResume). But you could set a listener, to get the value after the system calculate the pixel for the MATCH_PARENT.
Try this at your fragment's onViewCreated()
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
myView = view.findViewById(R.id.rtt); # your constraint layout
view.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
int height = bottom - top;
int width = right - left;
Log.i("LOG", "height is" + height);
Log.i("LOG", "width is " + width);
}
});
}
Anyway, notice you only get this value after the view is rendered. At this point the view is already rendered, you might not be able to edit the margin. You might want to rethink your approach.
Related
What is the wrong with this code?
row_height gives a value outside onLayoutChange* but gives zero inside CreateEvent
public class MainActivity extends AppCompatActivity {
private int row_height;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView = (TextView) findViewById(R.id.textView33);
textView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
row_height = bottom - top;
textView.setText("" + row_height); //this show a correct value
}
});
float Start_time = (float) 6;
int Event_duration_in_min = 120;
int day_number = 3;
CreateEvent(Event_duration_in_min, Start_time, day_number - 1);
}
private void CreateEvent(final int event_duration_in_min, final float start_time, final int day) {
TextView textView = (TextView) findViewById(R.id.textView27);
textView.setText("" + row_height); // this show 0 !!
}
}
how to fix this ?
private void CreateEvent(final int event_duration_in_min, final float start_time, final int day) {
TextView textView = (TextView) findViewById(R.id.textView27);//any textview from the horizontal grid
textView.setText("" + row_height);
//Now creating events here
textView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
RelativeLayout RL = (RelativeLayout) findViewById(R.id.RL);
final int width = right - left;
float scale = getApplicationContext().getResources().getDisplayMetrics().density;//const code line
int pixels_h = (int) (event_duration_in_min * scale + 0.5f);//const code line
TextView tv = new TextView(getApplication().getApplicationContext());
if(row_height == 120) {
RelativeLayout.LayoutParams Params1 = new RelativeLayout.LayoutParams(width, pixels_h * 2);//const code line
tv.setLayoutParams(Params1);
}
else {
RelativeLayout.LayoutParams Params1 = new RelativeLayout.LayoutParams(width, pixels_h);//const code line
tv.setLayoutParams(Params1);
}
tv.setBackgroundResource(R.color.Red);
tv.setText("attttttttef");
tv.setX(day * width);
tv.setY((float) (start_time * row_height));
RL.addView(tv);
}
});
}
The layout for your view has not occurred by the time you call CreateEvent(), so the height will be zero. If you want to call CreateEvent() once your row_height is determined, put it inside your OnLayoutChangeListener, and check that your row_height is greater than zero.
Based on your result, CreateEvent is called before the OnLyoutChangeListener is triggered. Therefore, textView27's text is set before the row_height is assigned in the onLayoutChange (default 0).
Change the signature of your CreateEvent method to receive the row_height as a parameter and change this line to:
CreateEvent(Event_duration_in_min, Start_time, day_number - 1, row_height);
I have the following classes:
public class MyGridView extends ViewGroup {
...
#Override
public void onSizeChanged(int xNew, int yNew, int xOld, int yOld) {
super.onSizeChanged(xNew, yNew, xOld, yOld);
// do calculations for drawing bounds of child views
}
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View child;
Rect rect;
for (int i=0; i < getChildCount(); i++) {
child = getChildAt(i);
rect = getBoundsAtChildIndex(i)
child.layout(rect.left, rect.top, rect.right, rect.bottom);
}
}
...
}
AND
public class MyAdapter {
public void setMyGridView(MyGridView myGridView) {
// add a single TextView to my grid view
textView = createTextView(context);
addTextViewToGrid(textView);
container.add(textView);
}
private TextView createTextView(Context context) {
TextView textView = new TextView(context);
textView.setText("1");
textView.setTextColor(0xffffffff);
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f);
textView.setTypeface(typeface);
textView.setGravity(Gravity.CENTER | Gravity.FILL);
// textView.setGravity(Gravity.RIGHT); // other tries to see gravity
// textView.setGravity(Gravity.FILL);
return textView;
}
private void addTextViewToGrid(TextView textView) {
myGridViewPtr.addView(textView, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
}
...
}
I have created a custom grid view and adapter, where its children draw to the screen to the correct size and location. However, their gravity, set in the createTextView() is not observed.
I can set a background image to the textView and see it fill its space in the grid.
In contrast, the text in textView always draws in its top left corner, and it always stays at the text size I set it at 12sp, rather than scale to fit using Gravity.FILL.
Preferably, the text would scale to fit and center within the textview.
EDIT
I have added the following method for onMeasure() in ViewGroup:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
doUpdateOnSizeChanged(width, height);
for (int i = 0; i < getChildCount(); i++)
getChildAt(i).measure((int) viewDim.x, (int) viewDim.y);
setMeasuredDimension(width, height);
}
The children are supposed to be the same height, width as each other. The android example code for ViewGroup is more sophisticated than I require. For instance, I am not getting the max of all children width, height.
For my onMeasure(), the child width, height in ViewDim is width, height integers that are computed in doUpdateOnSizeChanged to be less than getMeasuredWidth, getMeasuredHeight.
The result is now text aligns in the bottom-left corner of the TextView.
onMeasure needed minor corrections, basically needing to rely on MeasureSpec:
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width, height;
int cWidth1, cHeight1;
width = MeasureSpec.getSize(widthMeasureSpec);
height = MeasureSpec.getSize(heightMeasureSpec);
doUpdateOnSizeChanged(width, height);
cWidth1 = (int)viewDim.x; cHeight1 = (int)viewDim.y;
measureChildren(MeasureSpec.makeMeasureSpec(cWidth1, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(cHeight1, MeasureSpec.EXACTLY));
setMeasuredDimension(width, height);
}
I have a custom view that extends ViewGroup and I don't know why but I can't center the text of my TextView vertically.
This is how I do it:
TextView myTextView = new TextView(mContext);
myTextView.setBackgroundColor(0xFFFF9900);
myTextView.layout(left, top, right, bottom);
myTextView.setGravity(Gravity.CENTER); // with this, the text is centered horizontally but is still on top. There is plenty of space below
this.addView(myTextView);
I have do this a lot of time on LinearLayout or RelativeLayout, I should have missed something but what?
Edit:
#Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Point centerOfView = new Point(this.getMeasuredWidth()/2, this.getMeasuredHeight()/2);
Log.d(TAG, "onLayout changed:"+changed+" buttonDiametre:"+buttonDiametre);
for(int n=0; n< this.getChildCount(); n++){
View v = getChildAt(n);
if (v instanceof TextView) {
v.layout(centerOfView.x-buttonDiametre/2, centerOfView.y-buttonDiametre/2, centerOfView.x+buttonDiametre/2, centerOfView.y+buttonDiametre/2);
ViewGroup.LayoutParams textViewParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
v.setLayoutParams(textViewParams);
TextView txt = (TextView)v;
txt.setGravity(Gravity.CENTER);
}
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.d(TAG, "onMeasure");
int width = getMeasuredWidth();
int height = getMeasuredHeight();
buttonDiametre = (int) (this.getMeasuredWidth()/4);
Log.d(TAG, "buttonDiametre:"+buttonDiametre+" getChildCount:"+getChildCount());
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
child.measure(buttonDiametre, buttonDiametre);
}
setMeasuredDimension(width, height);
}
With this try to override onMeasure and onLayout, I see no changes except that the text has lost it center horizontal align. The text is now on top left even with setGravity set to center.
Otherwise, my textbox that are in a square form display well on the different positions. I know i have put them in center but i animate them later to get in correct pposition.
You need to set your TextView height to MATCH_PARENT, then it will center text vertically, too:
TextView myTextView = new TextView(mContext);
myTextView.setBackgroundColor(0xFFFF9900);
myTextView.layout(left, top, right, bottom);
ViewGroup.LayoutParams textViewParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
myTextView.setLayoutParams(textViewParams);
myTextView.setGravity(Gravity.CENTER); // with this, the text is centered horizontally but is still on top. There is plenty of space below
this.addView(myTextView);
I have a FrameLayout, when I try to get its params like this:
ViewGroup.LayoutParams params = cameraPreviewFrameLayout.getLayoutParams();
int layoutHeight = params.height;
int layoutWidth = params.width;
Here, layoutHeight is -2 and layoutWidth is -1. This is my XML:
<FrameLayout
android:id="#+id/liveActivity_cameraPreviewFrameLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
I am performing this action from onCreate, but also I tried to to it through onStart(), with the same result. Obviously the height and width of this layout is not these values.
How can I retrieve the size of the layout effectively?
The values returned are correct, because:
-2 stands for LayoutParams.WRAP_CONENT
-1 stands for LayoutParams.MATCH_PARENT
If you want to get exact values, you'll need to use the methods getHeight and getWidth. However those methods can only be used once the layout has been measured, so delay your call like this:
cameraPreviewFrameLayout.post(new Runnable() {
#Override
public void run() {
View v = cameraPreviewFrameLayout;
Log.e("TAG", v.getWidth() + ":" + v.getHeight());
}
});
Proper way to getting size of layout after or inside onLayoutChange method call
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final FrameLayout layout = (FrameLayout) findViewById(R.id.liveActivity_cameraPreviewFrameLayout);
layout.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
#Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
int width = right - left;
int height = bottom - top;
Log.v("TAG", String.format("%d - %d", width, height));
//Or
Log.v("TAG", String.format("%d - %d", layout.getWidth(), layout.getHeight()));
//And after this method call, calling layout.getWidth() and layout.getHeight() will give right values
}
});
}
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();
}
}
}