How can I do something like a FlowLayout in Android?
You should use FlexboxLayout
with flexWrap="wrap"
attribute.
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:flexWrap="wrap">
<!-- contents go here -->
</com.google.android.flexbox.FlexboxLayout>
For build instructions, see the github repo.
implementation 'com.google.android:flexbox:2.0.1'
More about this - https://android-developers.googleblog.com/2017/02/build-flexible-layouts-with.html
I don't have enough reputation to post a comment to Romain Guy's answer but that's where this answer should be (I created an account just to share my edit).
Anyway, I see other people have found out his pretty cool FlowLayout solution has some issues. I could find one myself and I saw, as others, that some children were clipped. Looking in details at the algorithm it seems to be a very simple mistake in the calculation of the height. When the very last child is the one being put on a new line, then the height was not properly computed. I cleaned up a bit the computation (there was a weird use of "height" vs. currentHeight).
The following change fixes the problem of "last child is clipped if on a new line":
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
int widthLimit = MeasureSpec.getSize(widthMeasureSpec) - getPaddingRight();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
boolean growHeight = widthMode != MeasureSpec.UNSPECIFIED;
int width = 0;
int currentWidth = getPaddingLeft();
int currentHeight = getPaddingTop();
int maxChildHeight = 0;
boolean breakLine = false;
boolean newLine = false;
int spacing = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++)
{
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
spacing = mHorizontalSpacing;
if (lp.horizontalSpacing >= 0)
{
spacing = lp.horizontalSpacing;
}
if (growHeight && (breakLine || ((currentWidth + child.getMeasuredWidth()) > widthLimit)))
{
newLine = true;
currentHeight += maxChildHeight + mVerticalSpacing;
width = Math.max(width, currentWidth - spacing);
currentWidth = getPaddingLeft();
maxChildHeight = 0;
}
else
{
newLine = false;
}
maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight());
lp.x = currentWidth;
lp.y = currentHeight;
currentWidth += child.getMeasuredWidth() + spacing;
breakLine = lp.breakLine;
}
if (newLine == false)
{
width = Math.max(width, currentWidth - spacing);
}
width += getPaddingRight();
int height = currentHeight + maxChildHeight + getPaddingBottom();
setMeasuredDimension(resolveSize(width, widthMeasureSpec),
resolveSize(height, heightMeasureSpec));
}
There is a library from Google, called "flexbox-layout". You should check it out.
To use it in RecyclerView, you can use something like that:
val layoutManager = FlexboxLayoutManager(activity)
layoutManager.flexDirection = FlexDirection.ROW
layoutManager.flexWrap = FlexWrap.WRAP
layoutManager.justifyContent = JustifyContent.FLEX_START
layoutManager.alignItems = AlignItems.FLEX_START
recyclerView.layoutManager=layoutManager
Here is the custom class where you can achive layout like following with adding dynamicaly view (Also called FlowLayout).
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/*
Created By Dhavalkumar Solanki
* */
public class FlowLayout extends ViewGroup {
private int line_height_space;
public static class LayoutParams extends ViewGroup.LayoutParams {
public int horizontal_spacing;
public int vertical_spacing;
/**
* @param horizontal_spacing Pixels between items, horizontally
* @param vertical_spacing Pixels between items, vertically
*/
public LayoutParams(int horizontal_spacing, int vertical_spacing) {
super(0, 0);
this.horizontal_spacing = horizontal_spacing;
this.vertical_spacing = vertical_spacing;
}
}
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
assert (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED);
final int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
final int count = getChildCount();
int line_height_space = 0;
int xpos = getPaddingLeft();
int ypos = getPaddingTop();
int childHeightMeasureSpec;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST);
} else {
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), childHeightMeasureSpec);
final int childw = child.getMeasuredWidth();
line_height_space = Math.max(line_height_space, child.getMeasuredHeight() + lp.vertical_spacing);
if (xpos + childw > width) {
xpos = getPaddingLeft();
ypos += line_height_space;
}
xpos += childw + lp.horizontal_spacing;
}
}
this.line_height_space = line_height_space;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
height = ypos + line_height_space;
} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
if (ypos + line_height_space < height) {
height = ypos + line_height_space;
}
}
setMeasuredDimension(width, height);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(1, 1); // default of 1px spacing
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
if (p instanceof LayoutParams) {
return true;
}
return false;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
final int width = r - l;
int xpos = getPaddingLeft();
int ypos = getPaddingTop();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final int childw = child.getMeasuredWidth();
final int childh = child.getMeasuredHeight();
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (xpos + childw > width) {
xpos = getPaddingLeft();
ypos += line_height_space;
}
child.layout(xpos, ypos, xpos + childw, ypos + childh);
xpos += childw + lp.horizontal_spacing;
}
}
}
}
Example :
text_view.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tool="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:id="@+id/tvText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="19sp"
android:background="@drawable/unselected_tag"
android:textColor="@color/colorPrimary"
tool:text="Temp" />
</RelativeLayout>
activity_flow_layou_demo.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitleBusiness"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Business Interest "
android:textColor="@color/colorPrimary"
android:textSize="25sp" />
<com.example.tristateandroid2.radardemo.FlowLayout
android:id="@+id/flowBusiness"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.example.tristateandroid2.radardemo.FlowLayout>
</LinearLayout>
<LinearLayout
android:layout_marginTop="@dimen/activity_horizontal_margin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/tvTitlePrivate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Private Interest "
android:textColor="@color/colorPrimary"
android:textSize="25sp" />
<com.example.tristateandroid2.radardemo.FlowLayout
android:id="@+id/flowPrivate"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</com.example.tristateandroid2.radardemo.FlowLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</RelativeLayout>
FlowLayouDemo.java
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.ArrayList;
public class FlowLayouDemo extends AppCompatActivity {
private TextView tvTitleBusiness;
private FlowLayout flowBusiness;
private TextView tvTitlePrivate;
private FlowLayout flowPrivate;
private ArrayList<TagModel> arrayList;
private void findViews() {
tvTitleBusiness = (TextView) findViewById(R.id.tvTitleBusiness);
flowBusiness = (FlowLayout) findViewById(R.id.flowBusiness);
tvTitlePrivate = (TextView) findViewById(R.id.tvTitlePrivate);
flowPrivate = (FlowLayout) findViewById(R.id.flowPrivate);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flow_layou_demo);
findViews();
addLayouts();
}
private void addLayouts() {
if (arrayList == null) {
arrayList = new ArrayList<>();
}
flowBusiness.removeAllViews();
flowPrivate.removeAllViews();
for (int i = 0; i < 75; i++) {
final boolean[] selected = {false};
View view = this.getLayoutInflater().inflate(R.layout.text_view, null);
final TextView textView = (TextView) view.findViewById(R.id.tvText);
if (i % 5 == 0) {
arrayList.add(new TagModel(i, false, "Business VIEW : " + i));
textView.setText("Busi VIEW To IS : " + i);
} else {
arrayList.add(new TagModel(i, false, "TEXT IS : " + i));
textView.setText("Busi IS : " + i);
}
textView.setBackgroundResource(R.drawable.unselected_tag);
textView.setTextColor(Color.parseColor("#3F51B5"));
textView.setTag(i);
if(i<=50){
flowBusiness.addView(view);
}else {
textView.setText("Priv View : "+i);
flowPrivate.addView(view);
}
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (selected[0]) {
selected[0] = false;
textView.setBackgroundResource(R.drawable.unselected_tag);
textView.setTextColor(Color.parseColor("#3F51B5"));
} else {
selected[0] = true;
textView.setBackgroundResource(R.drawable.selected_tag);
textView.setTextColor(Color.parseColor("#FFFFFF"));
}
}
});
}
}
}