I've had similar issue, but solved this using GridLayout. The minuses that all views should be created from the start:
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.GridLayout
android:id="@+id/bubble_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="vertical"
android:scrollbarStyle="outsideOverlay"
android:layout_gravity="top|left"
app:orientation="vertical"
app:columnCount="@integer/grid_cells">
</android.support.v7.widget.GridLayout>
</ScrollView>
Here is an adapter:
public class GridAdapter {
/**
* Indicates maximum filled row in current column, column is the index of arr, row is the value
* [224444555550000000]
* It equals to:
* [***********-------]
* [***********-------]
* [--*********-------]
* [--*********-------]
* [------*****-------]
* [------------------]
*/
private final int[] mRowMatrix;
public GridAdapter(GridLayout gridLayout) {
mGridLayout = gridLayout;
mPlacedList = new ArrayList<>(10);
mUnplacedList = new LinkedList<>();
mNumColumns = ResHelper.getInteger(R.integer.grid_cells);
mMinCells = ResHelper.getInteger(R.integer.min_cells);
mMaxCells = ResHelper.getInteger(R.integer.max_cells);
mRowMatrix = new int[mNumColumns];
}
public void notifyDataSetChanged() {
while (mUnplacedList.size() > 0) {
final int toCol = findColWithMinRow();
final int gapSize = findGapSizeForCol(toCol);
final CustomView view = findAppropriate(gapSize);
if (view == null) {
final int filledRow = toCol > 0
? (toCol + gapSize < mRowMatrix.length ? Math.min(mRowMatrix[toCol - 1], mRowMatrix[toCol + gapSize]) : mRowMatrix[toCol - 1])
: mRowMatrix[gapSize];
for (int j = toCol, jCount = toCol + gapSize; j < jCount; j++) {
mRowMatrix[j] = filledRow;
}
} else {
placeView(view, toCol, mRowMatrix[toCol]);
}
}
}
/**
* Put view in certain column and row in the gridlayout
*/
private void placeView(CustomView view, int toCol, int toRow) {
final int gridSize = view.getGridSize();
final GridLayout.LayoutParams params = new GridLayout.LayoutParams();
params.width = params.height = gridSize * mCellSize;
params.columnSpec = GridLayout.spec(toCol, gridSize);
params.rowSpec = GridLayout.spec(toRow, gridSize);
mPlacedList.add(view);
mUnplacedList.remove(view);
mGridLayout.addView(view, params);
final int filledRow = toRow + gridSize;
for (int j = toCol, count = toCol + gridSize; j < count; j++) {
mRowMap[j] = filledRow;
}
}
/**
* Find empty gap which starts from toCol
* [*********************]
* [******------*****----] here col = 6, size = 3
* [******------*****----]
* [******------*****----]
* [*****************----]
* @param toCol
* @return
*/
private int findGapSizeForCol(int toCol) {
final int fromRow = mRowMatrix[toCol];
int i = toCol;
for (; i < mNumColumns; i++) {
if (fromRow != mRowMatrix[i]) {
break;
}
}
return i - toCol;
}
/**
* Find column with minimum filled row
* [*********************]
* [*****************----] here col = 17
* [******------*****----]
* [******------*****----]
* @return
*/
private int findColWithMinRow() {
int minRow = Integer.MAX_VALUE, minCol = 0;
for (int i = 0, count = mRowMatrix.length; i < count; i++) {
if (minRow > mRowMatrix[i]) {
minRow = mRowMatrix[i];
minCol = i;
}
}
return minCol;
}
/**
* Find customView with appropriate size for the empty gap
*/
private CustomView findAppropriate(int size) {
for (int j = mUnplacedList.size() - 1; j >= 0; j--) {
if (mUnplacedList.get(j).getGridSize() <= size) {
return mUnplacedList.get(j);
}
}
return null;
}
}