greenfrvr / hashtag-view

Android fully customizable widget for representing data like hashtags collection and similiar.
MIT License
337 stars 54 forks source link

Add and remove tags #29

Open kerner1000 opened 7 years ago

kerner1000 commented 7 years ago

What would be a neat way to add and remove tags graphically? How could I keep one editable Tag at the end of the list to add another item?

Pawanyadav1 commented 2 years ago

package com.maestros.twikimodels.hashtagview;

import android.animation.LayoutTransition; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.Drawable;

import android.text.TextUtils; import android.util.AttributeSet; import android.util.SparseArray; import android.util.TypedValue; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView;

import androidx.annotation.ColorRes; import androidx.annotation.DimenRes; import androidx.annotation.DrawableRes; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat;

import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Multimap; import com.maestros.twikimodels.R;

import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List;

public class HashtagView extends LinearLayout {

@IntDef({GRAVITY_LEFT, GRAVITY_CENTER, GRAVITY_RIGHT})
@Retention(RetentionPolicy.SOURCE)
public @interface GravityMode {
}

@SuppressLint("RtlHardcoded")
public static final int GRAVITY_LEFT = Gravity.LEFT;
@SuppressLint("RtlHardcoded")
public static final int GRAVITY_RIGHT = Gravity.RIGHT;
public static final int GRAVITY_CENTER = Gravity.CENTER;

@IntDef({MODE_WRAP, MODE_STRETCH, MODE_EQUAL})
@Retention(RetentionPolicy.SOURCE)
public @interface StretchMode {
}

public static final int MODE_WRAP = 0;
public static final int MODE_STRETCH = 1;
public static final int MODE_EQUAL = 2;

@IntDef({DISTRIBUTION_LEFT, DISTRIBUTION_MIDDLE, DISTRIBUTION_RIGHT, DISTRIBUTION_RANDOM, DISTRIBUTION_NONE})
@Retention(RetentionPolicy.SOURCE)
public @interface RowDistribution {
}

public static final int DISTRIBUTION_LEFT = 0;
public static final int DISTRIBUTION_MIDDLE = 1;
public static final int DISTRIBUTION_RIGHT = 2;
public static final int DISTRIBUTION_RANDOM = 3;
public static final int DISTRIBUTION_NONE = 4;

@IntDef({ELLIPSIZE_START, ELLIPSIZE_MIDDLE, ELLIPSIZE_END, ELLIPSIZE_MARQUEE})
@Retention(RetentionPolicy.SOURCE)
public @interface Ellipsize {
}

public static final int ELLIPSIZE_START = 0;
public static final int ELLIPSIZE_MIDDLE = 1;
public static final int ELLIPSIZE_END = 2;
public static final int ELLIPSIZE_MARQUEE = 3;

@IntDef({COMPOSE_ORIGIN, COMPOSE_LINEAR})
@Retention(RetentionPolicy.SOURCE)
public @interface Compose {
}

public static final int COMPOSE_ORIGIN = 0;
public static final int COMPOSE_LINEAR = 1;

private static final SparseArray<TextUtils.TruncateAt> ellipsizeList = new SparseArray<>(4);

static {
    ellipsizeList.put(ELLIPSIZE_START, TextUtils.TruncateAt.START);
    ellipsizeList.put(ELLIPSIZE_MIDDLE, TextUtils.TruncateAt.MIDDLE);
    ellipsizeList.put(ELLIPSIZE_END, TextUtils.TruncateAt.END);
    ellipsizeList.put(ELLIPSIZE_MARQUEE, TextUtils.TruncateAt.MARQUEE);
}

private final LayoutParams rowLayoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
private final LayoutParams itemLayoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
private final FrameLayout.LayoutParams itemFrameParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
private LayoutTransition layoutTransition;

private List<TagsClickListener> clickListeners;
private List<TagsSelectListener> selectListeners;

private List<Float> widthList;
private List<ItemData> data;
private Multimap<Integer, ItemData> viewMap;
private SortState sortState = SortState.initState();

private ColorStateList itemTextColorStateList;
private int itemMargin;
private int itemPaddingLeft;
private int itemPaddingRight;
private int itemPaddingTop;
private int itemPaddingBottom;
private int itemDrawablePadding;
private int minItemWidth;
private int maxItemWidth;
private int itemTextGravity;
private int itemTextEllipsize;
private float itemTextSize;

private int rowMargin;
private int rowGravity;
private int rowDistribution;
private int rowMode;
private int rowCount;
private int composeMode;
private int backgroundDrawable;
private int foregroundDrawable;
private int leftDrawable;
private int leftSelectedDrawable;
private int rightDrawable;
private int rightSelectedDrawable;

private Typeface typeface;

private float totalItemsWidth;
private int selectionLimit = -1;
private int selectedItemsCount = 0;

private boolean isInSelectMode;
private boolean isDynamic;

private DataTransform transformer = (DataTransform) DefaultTransform.newInstance();
private DataSelector selector = (DataSelector) DefaultSelector.newInstance();

private ComposeHelper composeHelper;

private final ViewTreeObserver.OnPreDrawListener preDrawListener = new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        if (isPrepared()) {
            wrap();
            sort();
            draw();
            getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
        }
        return true;
    }
};

public HashtagView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);

    setOrientation(VERTICAL);
    setGravity(Gravity.CENTER_HORIZONTAL);

    extractAttributes(attrs);
    prepareLayoutParams();
    prepareLayoutTransition();
    prepareComposeHelper();

    widthList = new ArrayList<>();
    data = new ArrayList<>();
}

/**
 * Method returns data that was set previously for that view.
 *
 * @return list of original data items
 */
@SuppressWarnings("unchecked")
public <T> List<T> getData() {
    List<T> list = new ArrayList<>();

    if (viewMap != null && !viewMap.isEmpty()) {
        for (ItemData itemData : viewMap.values()) {
            list.add(((T) itemData.data));
        }
    }

    return list;
}

/**
 * Method defines data as simple {@link java.lang.String} array. Using this method makes not
 * possible to use {@link android.text.Spannable} for representing items label.
 *
 * @param list {@link java.lang.String} array representing data collection.
 */
public <T> void setData(@NonNull List<T> list) {
    widthList.clear();
    data.clear();

    if (list.isEmpty()) {
        removeAllViews();
        return;
    }

    for (T item : list) {
        data.add(new ItemData<>(item));
    }
    getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}

/**
 * Method defines data as an array of custom data model. Using this method allow you
 * to use {@link android.text.Spannable} for representing items label.
 *
 * @param list        Array of user defined objects representing data collection.
 * @param transformer Implementation of {@link com.greenfrvr.hashtagview.HashtagView.DataTransform}  or
 *                    {@link com.greenfrvr.hashtagview.HashtagView.DataStateTransform}
 *                    interface. Can be used for building label from several custom data model
 *                    fields or to prepare {@link android.text.Spannable} label representation.
 * @param <T>         Custom data model
 */
public <T> void setData(@NonNull List<T> list, @NonNull DataTransform<T> transformer) {
    this.transformer = transformer;
    setData(list);
}

/**
 * Method defines data as an array of custom data model. Using this method allow you
 * to use {@link android.text.Spannable} for representing items label and define which items
 * should be preselected items.
 *
 * @param list        Array of user defined objects representing data collection.
 * @param transformer Implementation of {@link com.greenfrvr.hashtagview.HashtagView.DataTransform} or
 *                    {@link com.greenfrvr.hashtagview.HashtagView.DataStateTransform}
 *                    interface. Can be used for building label from several custom data model
 *                    fields or to prepare {@link android.text.Spannable} label representation.
 * @param selector    Implementation of {@link com.greenfrvr.hashtagview.HashtagView.DataSelector}
 *                    interface. Can be used to preselect some items.
 * @param <T>         Custom data model
 */
public <T> void setData(@NonNull List<T> list, @NonNull DataTransform<T> transformer, @NonNull DataSelector<T> selector) {
    this.selector = selector;
    setData(list, transformer);
}

/**
 * Dynamically adds new item to a widget.
 *
 * @param item Object representing new item to be added
 * @param <T>  Custom data model class
 * @return true if item was added successfully, false otherwise (for example the item
 * has been already presented)
 */
public <T> boolean addItem(@NonNull T item) {
    if (!isDynamic) return false;

    ItemData itemData = new ItemData<>(item);
    if (viewMap != null && viewMap.values().contains(itemData)) return false;

    if (viewMap != null) {
        data.addAll(viewMap.values());
        viewMap.clear();
    }
    data.add(itemData);

    getViewTreeObserver().addOnPreDrawListener(preDrawListener);
    return true;
}

/**
 * Dynamically removes given item from a widget if it is already presented in a widget.
 *
 * @param item Object representing item to be removed
 * @param <T>  Custom data model class
 * @return true if item was removed successfully, false otherwise (for example there
 * was no item to remove)
 */
public <T> boolean removeItem(@NonNull T item) {
    if (!isDynamic || viewMap == null || viewMap.isEmpty()) return false;

    ItemData itemData = new ItemData<>(item);
    if (!viewMap.values().contains(itemData)) return false;

    data.addAll(viewMap.values());
    data.remove(itemData);
    if (data.isEmpty()) removeAllViews();
    viewMap.clear();

    getViewTreeObserver().addOnPreDrawListener(preDrawListener);
    return true;
}

/**
 * Dynamically removes all previously added items from a widget.
 *
 * @return true if item was removed successfully, false otherwise (for example there
 * was no item to remove)
 */
public boolean removeAll() {
    if (!isDynamic || viewMap == null || viewMap.isEmpty()) return false;

    data.clear();
    viewMap.clear();
    removeAllViews();

    return true;
}

/**
 * @param transformer Implementation of {@link com.greenfrvr.hashtagview.HashtagView.DataTransform} or
 *                    {@link com.greenfrvr.hashtagview.HashtagView.DataStateTransform}
 *                    interface. Can be used for building label from several custom data model
 *                    fields or to prepare {@link android.text.Spannable} label representation.
 * @param <T>         Custom data model
 */
public <T> void setTransformer(@NonNull DataTransform<T> transformer) {
    this.transformer = transformer;
}

/**
 * @return List of selected items. Consists of objects corresponding to custom data model defined by setData() method
 */
@SuppressWarnings("unchecked")
public <T> List<T> getSelectedItems() {
    List<T> selected = new ArrayList<>();

    if (viewMap != null && !viewMap.isEmpty()) {
        for (ItemData item : viewMap.values()) {
            if (item.isSelected) selected.add((T) item.data);
        }
    }

    return selected;
}

/**
 * Adding single item click listener
 *
 * @param listener {@link com.greenfrvr.hashtagview.HashtagView.TagsClickListener}
 */
public void addOnTagClickListener(TagsClickListener listener) {
    if (clickListeners == null) {
        clickListeners = new ArrayList<>();
    }
    clickListeners.add(listener);
}

/**
 * Removing single item click listener
 *
 * @param listener {@link com.greenfrvr.hashtagview.HashtagView.TagsClickListener}
 */
public void removeOnTagClickListener(TagsClickListener listener) {
    if (clickListeners != null) {
        clickListeners.remove(listener);
    }
}

/**
 * Adding selection items listener
 *
 * @param listener {@link com.greenfrvr.hashtagview.HashtagView.TagsSelectListener}
 */
public void addOnTagSelectListener(TagsSelectListener listener) {
    if (selectListeners == null) {
        selectListeners = new ArrayList<>();
    }
    selectListeners.add(listener);
}

/**
 * Removing selection items listener
 *
 * @param listener {@link com.greenfrvr.hashtagview.HashtagView.TagsSelectListener}
 */
public void removeOnTagSelectListener(TagsSelectListener listener) {
    if (selectListeners != null) {
        selectListeners.remove(listener);
    }
}

/**
 * Removing all defined listeners
 */
public void removeListeners() {
    if (clickListeners != null) {
        clickListeners.clear();
    }

    if (selectListeners != null) {
        selectListeners.clear();
    }
}

public int getSelectionLimit() {
    return selectionLimit;
}

public void setSelectionLimit(int selectionLimit) {
    this.selectionLimit = selectionLimit > 0 ? selectionLimit : -1;

    if (viewMap != null) {
        for (ItemData item : viewMap.values()) {
            item.isSelected = false;
            item.displaySelection(leftDrawable, leftSelectedDrawable, rightDrawable, rightSelectedDrawable);
            item.decorateText(transformer);
        }
    }
}

public void setItemMargin(int itemMargin) {
    this.itemMargin = itemMargin;
}

public void setItemMarginRes(@DimenRes int marginRes) {
    this.itemMargin = getResources().getDimensionPixelOffset(marginRes);
}

public void setItemPadding(int left, int right, int top, int bottom) {
    this.itemPaddingLeft = left;
    this.itemPaddingRight = right;
    this.itemPaddingTop = top;
    this.itemPaddingBottom = bottom;
}

public void setItemPaddingRes(@DimenRes int left, @DimenRes int right, @DimenRes int top, @DimenRes int bottom) {
    this.itemPaddingLeft = getResources().getDimensionPixelOffset(left);
    this.itemPaddingRight = getResources().getDimensionPixelOffset(right);
    this.itemPaddingTop = getResources().getDimensionPixelOffset(top);
    this.itemPaddingBottom = getResources().getDimensionPixelOffset(bottom);
}

public void setMinItemWidth(int minWidth) {
    this.minItemWidth = minWidth;
}

public void setMinItemWidthRes(@DimenRes int minWidth) {
    this.minItemWidth = getResources().getDimensionPixelOffset(minWidth);
}

public void setMaxItemWidth(int maxWidth) {
    this.maxItemWidth = maxWidth;
}

public void setMaxItemWidthRes(@DimenRes int maxWidth) {
    this.maxItemWidth = getResources().getDimensionPixelOffset(maxWidth);
}

public void setItemTextColor(int textColor) {
    this.itemTextColorStateList = ColorStateList.valueOf(textColor);
}

public void setItemTextColorRes(@ColorRes int textColor) {
    int colorValue = ContextCompat.getColor(getContext(), textColor);
    this.itemTextColorStateList = ColorStateList.valueOf(colorValue);
}

public void setItemTextColorStateList(ColorStateList stateList) {
    this.itemTextColorStateList = stateList;
}

public void setItemTextColorStateListRes(@ColorRes int colorStateRes) {
    this.itemTextColorStateList = ContextCompat.getColorStateList(getContext(), colorStateRes);
}

public void setItemTextGravity(int itemTextGravity) {
    this.itemTextGravity = itemTextGravity;
}

public void setItemTextSize(float textSize) {
    this.itemTextSize = textSize;
}

public void setItemTextSizeRes(@DimenRes int textSize) {
    this.itemTextSize = getResources().getDimension(textSize);
}

public void setRowMargin(int rowMargin) {
    this.rowMargin = rowMargin;
}

public void setRowMarginRes(@DimenRes int rowMargin) {
    this.rowMargin = getResources().getDimensionPixelOffset(rowMargin);
}

public void setRowGravity(@GravityMode int rowGravity) {
    this.rowGravity = rowGravity;
}

public void setRowMode(@StretchMode int rowMode) {
    this.rowMode = rowMode;
}

public void setRowDistribution(@RowDistribution int rowDistribution) {
    this.rowDistribution = rowDistribution;
}

public void setComposeMode(@Compose int composeMode) {
    this.composeMode = composeMode;
    prepareComposeHelper();
}

public void setRowCount(int count) {
    if (count >= 0) this.rowCount = count;
}

public void setBackgroundDrawable(@DrawableRes int backgroundDrawable) {
    this.backgroundDrawable = backgroundDrawable;
}

public void setBackgroundColor(@ColorRes int backgroundDrawable) {
    this.backgroundDrawable = backgroundDrawable;
}

public void setForegroundDrawable(@DrawableRes int foregroundDrawable) {
    this.foregroundDrawable = foregroundDrawable;
}

public void setLeftDrawable(@DrawableRes int drawableRes) {
    this.leftDrawable = drawableRes;
}

public void setLeftSelectedDrawable(@DrawableRes int drawableRes) {
    this.leftSelectedDrawable = drawableRes;
}

public void setRightDrawable(@DrawableRes int drawableRes) {
    this.rightDrawable = drawableRes;
}

public void setRightSelectedDrawable(@DrawableRes int drawableRes) {
    this.rightSelectedDrawable = drawableRes;
}

public void setInSelectMode(boolean selectMode) {
    isInSelectMode = selectMode;
}

public void setDynamicMode(boolean dynamicMode) {
    isDynamic = dynamicMode;
}

public void setTypeface(Typeface typeface) {
    this.typeface = typeface;
}

public void setEllipsize(@Ellipsize int ellipsizeMode) {
    itemTextEllipsize = ellipsizeMode;
}

private void extractAttributes(AttributeSet attrs) {
    TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.HashtagView, 0, 0);
    try {
        itemMargin = a.getDimensionPixelOffset(R.styleable.HashtagView_tagMargin, getResources().getDimensionPixelOffset(R.dimen.default_item_margin));
        itemPaddingLeft = a.getDimensionPixelOffset(R.styleable.HashtagView_tagPaddingLeft, getResources().getDimensionPixelOffset(R.dimen.default_item_padding));
        itemPaddingRight = a.getDimensionPixelOffset(R.styleable.HashtagView_tagPaddingRight, getResources().getDimensionPixelOffset(R.dimen.default_item_padding));
        itemPaddingTop = a.getDimensionPixelOffset(R.styleable.HashtagView_tagPaddingTop, getResources().getDimensionPixelOffset(R.dimen.default_item_padding));
        itemPaddingBottom = a.getDimensionPixelOffset(R.styleable.HashtagView_tagPaddingBottom, getResources().getDimensionPixelOffset(R.dimen.default_item_padding));
        itemDrawablePadding = a.getDimensionPixelOffset(R.styleable.HashtagView_tagDrawablePadding, 0);
        minItemWidth = a.getDimensionPixelOffset(R.styleable.HashtagView_tagMinWidth, getResources().getDimensionPixelOffset(R.dimen.min_item_width));
        maxItemWidth = a.getDimensionPixelOffset(R.styleable.HashtagView_tagMaxWidth, getResources().getDimensionPixelOffset(R.dimen.min_item_width));
        rowMargin = a.getDimensionPixelOffset(R.styleable.HashtagView_rowMargin, getResources().getDimensionPixelOffset(R.dimen.default_row_margin));
        itemTextSize = a.getDimension(R.styleable.HashtagView_tagTextSize, getResources().getDimension(R.dimen.default_text_size));

        itemTextGravity = a.getInt(R.styleable.HashtagView_tagTextGravity, Gravity.CENTER);
        itemTextEllipsize = a.getInt(R.styleable.HashtagView_tagEllipsize, ELLIPSIZE_END);
        rowGravity = a.getInt(R.styleable.HashtagView_rowGravity, Gravity.CENTER);
        rowDistribution = a.getInt(R.styleable.HashtagView_rowDistribution, DISTRIBUTION_NONE);
        rowMode = a.getInt(R.styleable.HashtagView_rowMode, MODE_WRAP);
        rowCount = a.getInt(R.styleable.HashtagView_rowsQuantity, 0);
        composeMode = a.getInt(R.styleable.HashtagView_composeMode, COMPOSE_ORIGIN);

        backgroundDrawable = a.getResourceId(R.styleable.HashtagView_tagBackground, 0);
        foregroundDrawable = a.getResourceId(R.styleable.HashtagView_tagForeground, 0);
        leftDrawable = a.getResourceId(R.styleable.HashtagView_tagDrawableLeft, 0);
        leftSelectedDrawable = a.getResourceId(R.styleable.HashtagView_tagSelectedDrawableLeft, 0);
        rightDrawable = a.getResourceId(R.styleable.HashtagView_tagDrawableRight, 0);
        rightSelectedDrawable = a.getResourceId(R.styleable.HashtagView_tagSelectedDrawableRight, 0);

        itemTextColorStateList = a.getColorStateList(R.styleable.HashtagView_tagTextColor);
        if (itemTextColorStateList == null) {
            itemTextColorStateList = ColorStateList.valueOf(Color.BLACK);
        }

        isInSelectMode = a.getBoolean(R.styleable.HashtagView_selectionMode, false);
        isDynamic = a.getBoolean(R.styleable.HashtagView_dynamicMode, false);
    } finally {
        a.recycle();
    }
}

private void prepareLayoutParams() {
    itemFrameParams.gravity = itemTextGravity;

    itemLayoutParams.leftMargin = itemMargin;
    itemLayoutParams.rightMargin = itemMargin;
    itemLayoutParams.weight = rowMode > 0 ? 1 : 0;
    if (MODE_EQUAL == rowMode) {
        itemLayoutParams.width = 0;
    }

    rowLayoutParams.topMargin = rowMargin;
    rowLayoutParams.bottomMargin = rowMargin;
}

private void prepareLayoutTransition() {
    if (isDynamic) {
        layoutTransition = new LayoutTransition();
        layoutTransition.setStagger(LayoutTransition.APPEARING, 250);
        layoutTransition.setAnimateParentHierarchy(false);
    }
}

private void prepareComposeHelper() {
    switch (composeMode) {
        case COMPOSE_ORIGIN:
            composeHelper = new OriginalComposer();
            break;
        case COMPOSE_LINEAR:
            composeHelper = new LinearComposer();
            break;
    }
}

private boolean isPrepared() {
    return getViewWidth() > 0 || rowCount > 0;
}

private void wrap() {
    if (data == null || data.isEmpty()) return;
    widthList.clear();
    totalItemsWidth = 0;

    for (ItemData item : data) {
        wrapItem(item);
        widthList.add(item.width);
        totalItemsWidth += item.width;
    }

    composeHelper.prepareData();
}

private void wrapItem(ItemData item) {
    View view = inflateItemView(item);

    TextView itemView = (TextView) view.findViewById(R.id.text);
    itemView.setText(transformer.prepare(item.data));
    decorateItemTextView(itemView);

    float width = itemView.getMeasuredWidth() + drawableMetrics(itemView) + totalOffset();
    width = Math.max(width, minItemWidth);
    width = Math.min(width, getViewWidth() - 2 * totalOffset());
    item.view = view;
    item.width = width;
    setItemPreselected(item);
}

private void setItemPreselected(ItemData item) {
    if (isInSelectMode) {
        boolean selection = selector.preselect(item.data);
        if (selection) {
            if (selectionLimit == -1 || selectedItemsCount < selectionLimit) {
                selectedItemsCount += 1;
            } else {
                return;
            }
        }
        item.isSelected = selection;

        item.decorateText(transformer);
        item.displaySelection(leftDrawable, leftSelectedDrawable, rightDrawable, rightSelectedDrawable);
    }
}

private void sort() {
    if (data == null || data.isEmpty()) return;

    evaluateRowsQuantity();

    final int[] rowWidths = new int[sortState.totalRows()];
    viewMap = ArrayListMultimap.create(sortState.totalRows(), data.size());

    composeHelper.sortingLoop(0, sortState.rowCount, rowWidths, true);

    if (sortState.hasExtraRows) {
        composeHelper.sortingLoop(sortState.rowCount, sortState.totalRows(), rowWidths, false);
        sortState.release();
    }
}

private boolean extraCondition() {
    return !(sortState.hasExtraRows && data.size() == sortState.extraCount);
}

private void draw() {
    if (viewMap == null || viewMap.isEmpty()) return;
    removeAllViews();

    List<Integer> keys = new ArrayList<>(viewMap.keySet());
    Collections.sort(keys);

    for (Integer key : keys) {
        ViewGroup rowLayout = getRowLayout(viewMap.get(key).size());
        addView(rowLayout);
        applyDistribution(viewMap.get(key));
        rowLayout.setLayoutTransition(layoutTransition);

        for (ItemData item : viewMap.get(key)) {
            releaseParent(item.view);
            rowLayout.addView(item.view, itemLayoutParams);
        }
    }
    keys.clear();
}

private void releaseParent(View child) {
    ViewGroup parent = (ViewGroup) child.getParent();
    if (parent != null) {
        parent.removeView(child);
    }
}

private void applyDistribution(Collection<ItemData> list) {
    switch (rowDistribution) {
        case DISTRIBUTION_LEFT:
            Collections.sort((List) list);
            break;
        case DISTRIBUTION_MIDDLE:
            SortUtil.symmetricSort((List) list);
            break;
        case DISTRIBUTION_RIGHT:
            Collections.sort((List) list, Collections.reverseOrder());
            break;
        case DISTRIBUTION_RANDOM:
            Collections.shuffle((List) list);
            break;
        case DISTRIBUTION_NONE:
            break;
    }
}

private ViewGroup getRowLayout(int weightSum) {
    LinearLayout rowLayout = new LinearLayout(getContext());
    rowLayout.setLayoutParams(rowLayoutParams);
    rowLayout.setOrientation(HORIZONTAL);
    rowLayout.setGravity(rowGravity);
    rowLayout.setWeightSum(weightSum);
    return rowLayout;
}

private int getViewWidth() {
    return getWidth() - getPaddingLeft() - getPaddingRight();
}

private int totalOffset() {
    return itemPaddingLeft + itemPaddingRight + 2 * itemMargin;
}

private int drawableMetrics(TextView textView) {
    int drawablesWidth = 0;
    Drawable[] drawables = textView.getCompoundDrawables();
    drawablesWidth += drawables[0] != null ? drawables[0].getIntrinsicWidth() + itemDrawablePadding : 0;
    drawablesWidth += drawables[2] != null ? drawables[2].getIntrinsicWidth() + itemDrawablePadding : 0;
    return drawablesWidth;
}

private void evaluateRowsQuantity() {
    if (widthList == null || widthList.isEmpty()) return;

    if (rowCount > 0) {
        sortState.preserveState(rowCount);
        return;
    }

    composeHelper.evaluateRowsQuantity();
}

private View inflateItemView(final ItemData item) {
    ViewGroup itemLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.layout_item, this, false);
    itemLayout.setBackgroundResource(backgroundDrawable);
    itemLayout.setPadding(itemPaddingLeft, itemPaddingTop, itemPaddingRight, itemPaddingBottom);
    itemLayout.setMinimumWidth(minItemWidth);
    try {
        if (foregroundDrawable != 0)
            ((FrameLayout) itemLayout).setForeground(ContextCompat.getDrawable(getContext(), foregroundDrawable));
    } catch (Exception e) {
        e.printStackTrace();
    }

    itemLayout.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (isInSelectMode) {
                handleSelection(item);
            } else {
                handleClick(item);
            }
        }
    });
    return itemLayout;
}

private void decorateItemTextView(TextView textView) {
    textView.setTextColor(itemTextColorStateList);
    textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, itemTextSize);
    textView.setCompoundDrawablePadding(itemDrawablePadding);
    textView.setCompoundDrawablesWithIntrinsicBounds(leftDrawable, 0, rightDrawable, 0);
    textView.setEllipsize(ellipsizeList.get(itemTextEllipsize));
    if (maxItemWidth > 0) textView.setMaxWidth(maxItemWidth);
    if (typeface != null) textView.setTypeface(typeface);

    textView.setLayoutParams(itemFrameParams);
    textView.measure(0, 0);
}

private void handleSelection(ItemData item) {
    if (item.isSelected) {
        selectedItemsCount -= 1;
    } else {
        if (selectionLimit == -1 || selectedItemsCount < selectionLimit) {
            selectedItemsCount += 1;
        } else {
            return;
        }
    }

    item.select(leftDrawable, leftSelectedDrawable, rightDrawable, rightSelectedDrawable);
    item.decorateText(transformer);

    if (selectListeners != null) {
        for (TagsSelectListener listener : selectListeners) {
            listener.onItemSelected(item.data, item.isSelected);
        }
    }
}

private void handleClick(ItemData item) {
    if (clickListeners != null) {
        for (TagsClickListener listener : clickListeners) {
            listener.onItemClicked(item.data);
        }
    }
}

private class OriginalComposer implements ComposeHelper {

    @Override
    public void prepareData() {
        Collections.sort(data);
        Collections.sort(widthList, Collections.reverseOrder());
    }

    @Override
    public void evaluateRowsQuantity() {
        int rows = (int) Math.ceil(totalItemsWidth / getViewWidth());
        int[] rowsWidth = new int[rows];
        int iterationLimit = rows + widthList.size();
        int counter = 0;

        sortState.preserveState(rows);
        while (!widthList.isEmpty()) {
            rowIteration:
            for (int i = 0; i < rows; i++) {
                if (counter > iterationLimit) {
                    sortState.preserveState(rows, true, widthList.size());
                    widthList.clear();
                    return;
                }

                counter++;
                for (Float item : widthList) {
                    if (rowsWidth[i] + item <= getViewWidth()) {
                        rowsWidth[i] += item;
                        widthList.remove(item);
                        continue rowIteration;
                    }
                }
            }
        }
    }

    @Override
    public void sortingLoop(int start, int end, int[] widths, boolean hasExtra) {
        while (!data.isEmpty() && (!hasExtra || extraCondition())) {
            iteration:

            for (int i = start; i < end; i++) {
                Iterator<ItemData> iterator = data.iterator();

                while (iterator.hasNext()) {
                    ItemData item = iterator.next();
                    if (rowCount > 0 || widths[i] + item.width <= getViewWidth()) {
                        widths[i] += item.width;
                        viewMap.put(i, item);
                        iterator.remove();

                        if (hasExtra) continue iteration;
                    }
                }
            }
        }
    }
}

private class LinearComposer implements ComposeHelper {

    @Override
    public void prepareData() {
    }

    @Override
    public void evaluateRowsQuantity() {
    }

    @Override
    public void sortingLoop(int start, int end, int[] widths, boolean hasExtra) {
        if (data.isEmpty()) return;

        int rowWidth = 0;
        int index = start;
        Iterator<ItemData> it = data.iterator();

        while (it.hasNext()) {
            ItemData item = it.next();
            if (rowWidth + item.width > getViewWidth()) {
                index++;
                rowWidth = 0;
            }

            rowWidth += item.width;
            viewMap.put(index, item);
            it.remove();
        }
    }
}

private interface ComposeHelper {
    void prepareData();

    void evaluateRowsQuantity();

    void sortingLoop(int start, int end, int[] widths, boolean hasExtra);
}

/**
 * Listener used to handle item click events.
 */
public interface TagsClickListener {
    void onItemClicked(Object item);
}

/**
 * Listener used to handle item selection events.
 */
public interface TagsSelectListener {
    void onItemSelected(Object item, boolean selected);
}

/**
 * Prepare the formatting and appearance of data to be displayed on each item.
 * As it returns {@link CharSequence}, item text can be represented as a {@link android.text.SpannableString}.
 * Avoid using spans which may produce item width change (such as {@link android.text.style.BulletSpan} or {@link android.text.style.RelativeSizeSpan})
 */
public interface DataTransform<T> {
    CharSequence prepare(T item);
}

/**
 * Prepare the formatting and appearance of data to be displayed on each item, for both selected and
 * non-selected state.
 * As it returns {@link CharSequence}, item text can be represented as a {@link android.text.SpannableString}.
 * Avoid using spans which may produce item width change (such as {@link android.text.style.BulletSpan} or {@link android.text.style.RelativeSizeSpan})
 */
public interface DataStateTransform<T> extends DataTransform<T> {
    CharSequence prepareSelected(T item);
}

/**
 * Allows to define whether item should be preselected or not. Returning true (or false) for exact
 * item will cause initial state of this item to be set to selected (or unselected).
 */
public interface DataSelector<T> {
    boolean preselect(T item);
}

private static class SortState {
    boolean hasExtraRows = false;
    int extraCount = 0;
    int rowCount = 0;

    static SortState initState() {
        return new SortState();
    }

    int totalRows() {
        return (hasExtraRows ? extraCount : 0) + rowCount;
    }

    void preserveState(int rowCount, boolean needsExtraRow, int extraCount) {
        this.rowCount = rowCount;
        this.hasExtraRows = needsExtraRow;
        this.extraCount = extraCount;
    }

    void preserveState(int rowCount) {
        preserveState(rowCount, false, 0);
    }

    void release() {
        preserveState(0);
    }
}

}