harsh_v
harsh_v

Reputation: 3269

Create Tag Cloud

I need to create a basic Tag Cloud. My aim is to add multiple TextView(Tags) that automatically fits themselves in a new line if the number exceeds the device width. (Like Instagram).

Right now when the width of the layout containing (TextView)tags exceeds the (device width) the TextView(s) at the end wraps itself. the TextView at the last gets distorted

I tried using RelativeLayout but i am unable to figure out the logic. I viewed this post but if there is a better or simply a cleaner solution.

Thanks for your precious time.

It is not necessary that the result looks like Instagram. what i want to achieve

Upvotes: 4

Views: 4348

Answers (3)

Konstantin Konopko
Konstantin Konopko

Reputation: 5418

You can use androidx.constraintlayout.helper.widget.Flow component to make tag cloud easily.

        <androidx.constraintlayout.helper.widget.Flow
            android:id="@+id/flowPresets"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="@dimen/indent_page"
            app:flow_horizontalStyle="packed"
            app:flow_wrapMode="aligned"
            app:constraint_referenced_ids="buttonPresetStoreCloth,buttonPresetStoreFood"
            app:layout_constraintTop_toTopOf="@id/barrierRequestForm"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"/>

        <Button
            android:id="@+id/buttonPresetStoreCloth"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{handler::onClickMakePreset}"
            android:text="Cloth store" />

        <Button
            android:id="@+id/buttonPresetStoreFood"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{handler::onClickMakePreset}"
            android:text="Grocery store" />

Upvotes: 0

Nilanchala
Nilanchala

Reputation: 5941

The cleaner solution is to write your own custom ViewGroup class. Find the sample below.

For a complete descriptive explanation visit, How to Create Custom Layout in Android by Extending ViewGroup Class.

public class TagLayout extends ViewGroup {

    public TagLayout(Context context) {
        super(context);
    }

    public TagLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public TagLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        int curWidth, curHeight, curLeft, curTop, maxHeight;

        //get the available size of child view
        final int childLeft = this.getPaddingLeft();
        final int childTop = this.getPaddingTop();

        final int childRight = this.getMeasuredWidth() - this.getPaddingRight();
        final int childBottom = this.getMeasuredHeight() - this.getPaddingBottom();

        final int childWidth = childRight - childLeft;
        final int childHeight = childBottom - childTop;

        maxHeight = 0;
        curLeft = childLeft;
        curTop = childTop;
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() == GONE)
                return;

            //Get the maximum size of the child
            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
            curWidth = child.getMeasuredWidth();
            curHeight = child.getMeasuredHeight();
            //wrap is reach to the end
            if (curLeft + curWidth >= childRight) {
                curLeft = childLeft;
                curTop += maxHeight;
                maxHeight = 0;
            }
            //do the layout
            child.layout(curLeft, curTop, curLeft + curWidth, curTop + curHeight);
            //store the max height
            if (maxHeight < curHeight)
                maxHeight = curHeight;
            curLeft += curWidth;
        }
    }
}

To use the TagLayout, you can add it to your activity/fragment layout declaration. main_activity.xml

<com.javatechig.taglayout.TagLayout
    android:id="@+id/tagLayout"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

Now we can have a custom view to allow some level of customization for each tag item layout.

tag_layout.xml

<TextView
    android:id="@+id/tagTextView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="5dp"
    android:background="#a000"
    android:padding="10dp"
    android:textColor="#fff" />

MainActivity.java Finally, from activity class you can add the tag items as follows.

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TagLayout tagLayout = (TagLayout) findViewById(R.id.tagLayout);
        LayoutInflater layoutInflater = getLayoutInflater();
        String tag;
        for (int i = 0; i <= 20; i++) {
            tag = "#tag" + i;
            View tagView = layoutInflater.inflate(R.layout.tag_layout, null, false);

            TextView tagTextView = (TextView) tagView.findViewById(R.id.tagTextView);
            tagTextView.setText(tag);
            tagLayout.addView(tagView);
        }
    }
}

Result

enter image description here

Upvotes: 11

Lucas Santos
Lucas Santos

Reputation: 170

I believe the solution above always consider the whole screen width, so if the developer sets android:layout_width parameter for example it won't respect its value or even parent's margin and padding values.

I've fixed it as follows:

private int mScreenWidth = 0;
private int mAvailableWidth = -1;

public TagLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

private void init(Context context) {

    Display display = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
    Point deviceSize = new Point();
    display.getSize(deviceSize);

    mScreenWidth = deviceSize.x;
}

private void calculateAvailableWidth() {

    if(getLayoutParams() != null && getLayoutParams().width > 0) {
        mAvailableWidth = getLayoutParams().width;
        return;
    }

    mAvailableWidth = mScreenWidth;

    ViewGroup parent = this;

    while(parent != null) {

        mAvailableWidth -= parent.getPaddingLeft() + parent.getPaddingRight();

        if(parent.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
            ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)parent.getLayoutParams();
            mAvailableWidth -= layoutParams.leftMargin + layoutParams.rightMargin;
        }

        if(parent.getParent() instanceof ViewGroup)
            parent = (ViewGroup)parent.getParent();
        else
            parent = null;
    }
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    int currentRowWidth = 0;
    int currentRowHeight = 0;
    int maxItemWidth = 0;
    int maxWidth = 0;
    int maxHeight = 0;

    if(mAvailableWidth == -1)
        calculateAvailableWidth();

    for(int i = 0; i < count; i++) {
        View child = getChildAt(i);

        if(child.getVisibility() == GONE)
            continue;

        try {
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
        catch(Exception e) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }

        int childWidth = child.getMeasuredWidth() + child.getPaddingRight() + child.getPaddingLeft();
        int childHeight = child.getMeasuredHeight() + child.getPaddingTop() + child.getPaddingBottom();

        maxItemWidth = Math.max(maxItemWidth, childWidth);

        if(currentRowWidth + childWidth < mAvailableWidth) {
            currentRowWidth += childWidth;
            maxWidth = Math.max(maxWidth, currentRowWidth);
            currentRowHeight = Math.max(currentRowHeight, childHeight);
        }
        else {
            currentRowWidth = childWidth;
            maxHeight += currentRowHeight;
        }
    }

    if(getLayoutParams().width == LayoutParams.WRAP_CONTENT) {
        mAvailableWidth = maxItemWidth;
        maxWidth = maxItemWidth;
    }

    maxHeight += currentRowHeight + getPaddingTop() + getPaddingBottom();
    setMeasuredDimension(maxWidth, maxHeight);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    int count = getChildCount();

    int currentLeft = getPaddingLeft();
    int currentTop = getPaddingTop();
    int currentRight;
    int currentBottom;

    int parentWidth = this.getPaddingRight() - this.getPaddingLeft() + right;

    for(int i = 0; i < count; i++) {

        View child = getChildAt(i);

        if(child.getVisibility() == View.GONE)
            return;

        int currentWidth = child.getMeasuredWidth() + child.getPaddingRight() + child.getPaddingLeft();
        int currentHeight = child.getMeasuredHeight() + child.getPaddingBottom() + child.getPaddingTop();

        if(currentLeft + currentWidth > parentWidth) {
            currentLeft = getPaddingLeft();
            currentTop += currentHeight;
        }

        currentBottom = currentTop + currentHeight;
        currentRight = currentLeft + currentWidth;

        child.layout(currentLeft, currentTop, currentRight, currentBottom);

        currentLeft += currentWidth;
    }
}

This way you are able to set android:layout_width="250dp" and get a result like this:

enter image description here

Set android:layout_width="match_parent" and get a result like this:

enter image description here

Or event use android:layout_width="wrap_content and get a result like this:

enter image description here

Hope it helps.

Upvotes: 3

Related Questions