wfa.com.tma.wfa.chips.ChipsView.java Source code

Java tutorial

Introduction

Here is the source code for wfa.com.tma.wfa.chips.ChipsView.java

Source

/*
 * Copyright (C) 2016 Doodle AG.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package wfa.com.tma.wfa.chips;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import wfa.com.tma.wfa.R;
import wfa.com.tma.wfa.chips.model.Contact;
import wfa.com.tma.wfa.chips.views.ChipsEditText;
import wfa.com.tma.wfa.chips.views.ChipsVerticalLinearLayout;

public class ChipsView extends ScrollView implements ChipsEditText.InputConnectionWrapperInterface {
    private static final String TAG = "ChipsView";
    private static final int CHIP_HEIGHT = 32; // dp
    private static final int SPACING_TOP = 4; // dp
    private static final int SPACING_BOTTOM = 4; // dp
    public static final int DEFAULT_VERTICAL_SPACING = 1; // dp
    private static final int DEFAULT_MAX_HEIGHT = -1;

    private int mChipsBgRes = R.drawable.chip_background;

    private int mMaxHeight; // px
    private int mVerticalSpacing;

    private int mChipsBgColor;
    private int mChipsBgColorIndelible;
    private int mChipsBgColorClicked;
    private int mChipsTextColor;
    private int mChipsTextColorIndelible;
    private int mChipsTextColorClicked;
    private int mChipsDeleteBtnBgColor;
    private int mChipsDeleteBtnResColor;
    private int mChipsDeleteBtnBgColorClicked;
    private @ColorInt int mChipsPlaceholderTint;
    private int mChipsDeleteBtnResId;
    private String mChipsHintText;

    private int mChipsMargin;

    private float mDensity;
    private RelativeLayout mChipsContainer;
    private ChipsListener mChipsListener;
    private ChipsEditText mEditText;
    private ChipsVerticalLinearLayout mRootChipsLayout;
    private EditTextListener mEditTextListener;
    private List<Chip> mChipList = new ArrayList<>();
    private Object mCurrentEditTextSpan;
    private Typeface mTypeface;

    // initials
    private boolean mUseInitials = false;
    private int mInitialsTextSize;
    private Typeface mInitialsTypeface;
    @ColorInt
    private int mInitialsTextColor;

    public ChipsView(Context context) {
        super(context);
        init();
    }

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

    public ChipsView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initAttr(context, attrs);
        init();
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public ChipsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initAttr(context, attrs);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        if (mMaxHeight != DEFAULT_MAX_HEIGHT) {
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST);
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
        return true;
    }

    //<editor-fold desc="Initialization">
    private void initAttr(Context context, AttributeSet attrs) {
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ChipsView, 0, 0);
        try {
            mMaxHeight = a.getDimensionPixelSize(R.styleable.ChipsView_cv_max_height, DEFAULT_MAX_HEIGHT);
            mVerticalSpacing = a.getDimensionPixelSize(R.styleable.ChipsView_cv_vertical_spacing,
                    (int) (DEFAULT_VERTICAL_SPACING * mDensity));
            mChipsBgColor = a.getColor(R.styleable.ChipsView_cv_bg_color,
                    ContextCompat.getColor(context, R.color.chip_bg));
            mChipsBgColorClicked = a.getColor(R.styleable.ChipsView_cv_bg_color_clicked,
                    ContextCompat.getColor(context, R.color.blue));
            mChipsBgColorIndelible = a.getColor(R.styleable.ChipsView_cv_bg_color_indelible, mChipsBgColor);
            mChipsTextColor = a.getColor(R.styleable.ChipsView_cv_text_color, Color.BLACK);
            mChipsTextColorClicked = a.getColor(R.styleable.ChipsView_cv_text_color_clicked, Color.WHITE);
            mChipsTextColorIndelible = a.getColor(R.styleable.ChipsView_cv_text_color_indelible, mChipsTextColor);
            mChipsPlaceholderTint = a.getColor(R.styleable.ChipsView_cv_icon_placeholder_tint, 0);
            mChipsDeleteBtnResId = a.getResourceId(R.styleable.ChipsView_cv_icon_delete,
                    R.drawable.ic_chip_close_24dp);
            mChipsDeleteBtnResColor = a.getResourceId(R.styleable.ChipsView_cv_icon_delete_color, Color.WHITE);
            mChipsDeleteBtnBgColor = a.getResourceId(R.styleable.ChipsView_cv_icon_delete_bg_color, Color.GRAY);
            mChipsDeleteBtnBgColorClicked = a.getResourceId(R.styleable.ChipsView_cv_icon_delete_bg_clicked_color,
                    ContextCompat.getColor(context, R.color.mediumBlue));
            mChipsHintText = a.getString(R.styleable.ChipsView_cv_text_hint);
            mChipsMargin = a.getDimensionPixelSize(R.styleable.ChipsView_cv_chips_margin, 0);
        } finally {
            a.recycle();
        }
    }

    private void init() {
        mDensity = getResources().getDisplayMetrics().density;

        mChipsContainer = new RelativeLayout(getContext());
        addView(mChipsContainer);

        // Dummy item to prevent AutoCompleteTextView from receiving focus
        LinearLayout linearLayout = new LinearLayout(getContext());
        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(0, 0);
        linearLayout.setLayoutParams(params);
        linearLayout.setFocusable(true);
        linearLayout.setFocusableInTouchMode(true);

        mChipsContainer.addView(linearLayout);

        mEditText = new ChipsEditText(getContext(), this);
        RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        layoutParams.topMargin = (int) (SPACING_TOP * mDensity);
        layoutParams.bottomMargin = (int) (SPACING_BOTTOM * mDensity) + mVerticalSpacing;
        mEditText.setLayoutParams(layoutParams);
        mEditText.setMinHeight((int) (CHIP_HEIGHT * mDensity));
        mEditText.setPadding(0, 0, 0, 0);
        mEditText.setLineSpacing(mVerticalSpacing, (CHIP_HEIGHT * mDensity) / mEditText.getLineHeight());
        mEditText.setBackgroundColor(Color.argb(0, 0, 0, 0));
        mEditText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI | EditorInfo.IME_ACTION_UNSPECIFIED);
        mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
                | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
        mEditText.setHint(mChipsHintText);

        mChipsContainer.addView(mEditText);

        mRootChipsLayout = new ChipsVerticalLinearLayout(getContext(), mVerticalSpacing);
        mRootChipsLayout.setOrientation(LinearLayout.VERTICAL);
        mRootChipsLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));
        mRootChipsLayout.setPadding(0, (int) (SPACING_TOP * mDensity), 0, 0);
        mChipsContainer.addView(mRootChipsLayout);

        initListener();

        if (isInEditMode()) {
            // preview chips
            LinearLayout editModeLinLayout = new LinearLayout(getContext());
            editModeLinLayout.setOrientation(LinearLayout.HORIZONTAL);
            mChipsContainer.addView(editModeLinLayout);

            View view = new Chip("Test Chip", null, new Contact(null, null, "Test", "asd@asd.de", null)).getView();
            view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            editModeLinLayout.addView(view);

            View view2 = new Chip("Indelible", null, new Contact(null, null, "Test", "asd@asd.de", null), true)
                    .getView();
            view2.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
            editModeLinLayout.addView(view2);
        }
    }

    private void initListener() {
        mChipsContainer.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                mEditText.requestFocus();
                unselectAllChips();
            }
        });

        mEditTextListener = new EditTextListener();
        mEditText.addTextChangedListener(mEditTextListener);
        mEditText.setOnFocusChangeListener(new OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (hasFocus) {
                    unselectAllChips();
                }
            }
        });
    }

    public void addChip(String displayName, String avatarUrl, Contact contact) {
        addChip(displayName, Uri.parse(avatarUrl), contact);
    }

    public void addChip(String displayName, Uri avatarUrl, Contact contact) {
        addChip(displayName, avatarUrl, contact, false);
        mEditText.setText("");
        addLeadingMarginSpan();
    }

    public void addChip(String displayName, Uri avatarUrl, Contact contact, boolean isIndelible) {
        Chip chip = new Chip(displayName, avatarUrl, contact, isIndelible);
        mChipList.add(chip);
        if (mChipsListener != null) {
            mChipsListener.onChipAdded(chip);
        }

        mEditText.setHint(null);

        onChipsChanged(true);
        post(new Runnable() {
            @Override
            public void run() {
                fullScroll(View.FOCUS_DOWN);
            }
        });
    }

    public void setTypeface(@NonNull Typeface typeface) {
        this.mTypeface = typeface;
        if (mEditText != null) {
            mEditText.setTypeface(mTypeface);
        }
    }

    /**
     * Use Initials instead of the person icon.
     *
     * @param textSize         in SP
     * @param initialsTypeface Nullable typeface
     */
    public void useInitials(int textSize, @Nullable Typeface initialsTypeface, @ColorInt int textColor) {
        this.mUseInitials = true;
        this.mInitialsTextSize = textSize;
        this.mInitialsTypeface = initialsTypeface;
        this.mInitialsTextColor = textColor;
    }

    public void clearText() {
        mEditText.setText("");
        onChipsChanged(true);
    }

    @NonNull
    public List<Chip> getChips() {
        return Collections.unmodifiableList(mChipList);
    }

    public boolean removeChipBy(Contact contact) {
        for (int i = 0; i < mChipList.size(); i++) {
            if (mChipList.get(i).mContact != null && mChipList.get(i).mContact.equals(contact)) {
                mChipList.remove(i);
                if (mChipList.isEmpty()) {
                    mEditText.setHint(mChipsHintText);
                }
                onChipsChanged(true);
                return true;
            }
        }
        return false;
    }

    public void setChipsListener(ChipsListener chipsListener) {
        this.mChipsListener = chipsListener;
    }

    public EditText getEditText() {
        return mEditText;
    }
    //</editor-fold>

    //<editor-fold desc="Private Methods">
    /**
     * rebuild all chips and place them right
     */
    private void onChipsChanged(final boolean moveCursor) {
        ChipsVerticalLinearLayout.TextLineParams textLineParams = mRootChipsLayout.onChipsChanged(mChipList);

        // if null then run another layout pass
        if (textLineParams == null) {
            post(new Runnable() {
                @Override
                public void run() {
                    onChipsChanged(moveCursor);
                }
            });
            return;
        }

        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mEditText.getLayoutParams();
        params.topMargin = (int) ((SPACING_TOP + textLineParams.row * CHIP_HEIGHT) * mDensity)
                + textLineParams.row * mVerticalSpacing;
        mEditText.setLayoutParams(params);
        addLeadingMarginSpan(textLineParams.lineMargin + mChipsMargin * textLineParams.chipsCount);
        if (moveCursor) {
            mEditText.setSelection(mEditText.length());
        }
    }

    private void addLeadingMarginSpan(int margin) {
        Spannable spannable = mEditText.getText();
        if (mCurrentEditTextSpan != null) {
            spannable.removeSpan(mCurrentEditTextSpan);
        }
        mCurrentEditTextSpan = new android.text.style.LeadingMarginSpan.LeadingMarginSpan2.Standard(margin, 0);
        spannable.setSpan(mCurrentEditTextSpan, 0, 0, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        mEditText.setText(spannable);
    }

    private void addLeadingMarginSpan() {
        Spannable spannable = mEditText.getText();
        if (mCurrentEditTextSpan != null) {
            spannable.removeSpan(mCurrentEditTextSpan);
        }
        spannable.setSpan(mCurrentEditTextSpan, 0, 0, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        mEditText.setText(spannable);
    }

    /**
     * return true if the text should be deleted
     */
    private boolean onEnterPressed(String text) {
        boolean shouldDeleteText = true;
        if (text != null && text.length() > 0) {
            onEmailRecognized(text);
            if (shouldDeleteText) {
                mEditText.setSelection(0);
            }
        }
        return shouldDeleteText;
    }

    private void onEmailRecognized(String email) {
        onEmailRecognized(new Contact(email, "", null, email, null));
    }

    private void onEmailRecognized(Contact contact) {
        Chip chip = new Chip(contact.getDisplayName(), null, contact);
        mChipList.add(chip);
        if (mChipsListener != null) {
            mChipsListener.onChipAdded(chip);
        }
        post(new Runnable() {
            @Override
            public void run() {
                onChipsChanged(true);
            }
        });
    }

    private void selectOrDeleteLastChip() {
        if (mChipList.size() > 0) {
            onChipInteraction(mChipList.size() - 1);
        }
    }

    private void onChipInteraction(int position) {
        try {
            Chip chip = mChipList.get(position);
            if (chip != null) {
                onChipInteraction(chip, true);
            }
        } catch (IndexOutOfBoundsException e) {
            Log.e(TAG, "Out of bounds", e);
        }
    }

    private void onChipInteraction(Chip chip, boolean nameClicked) {
        unselectChipsExcept(chip);
        if (chip.isSelected()) {
            mChipList.remove(chip);
            if (mChipsListener != null) {
                mChipsListener.onChipDeleted(chip);
            }
            onChipsChanged(true);
            if (nameClicked) {
                mEditText.setText(chip.getContact().getEmailAddress());
                addLeadingMarginSpan();
                mEditText.requestFocus();
                mEditText.setSelection(mEditText.length());
            }
        } else {
            chip.setSelected(true);
            onChipsChanged(false);
        }
    }

    private void unselectChipsExcept(Chip rootChip) {
        for (Chip chip : mChipList) {
            if (chip != rootChip) {
                chip.setSelected(false);
            }
        }
        onChipsChanged(false);
    }

    private void unselectAllChips() {
        unselectChipsExcept(null);
    }

    @Override
    public InputConnection getInputConnection(InputConnection target) {
        return new KeyInterceptingInputConnection(target);
    }

    private class EditTextListener implements TextWatcher {

        private boolean mIsPasteTextChange = false;

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            if (count > 1) {
                mIsPasteTextChange = true;
            }
        }

        @Override
        public void afterTextChanged(Editable s) {
            if (mIsPasteTextChange) {
                mIsPasteTextChange = false;
                // todo handle copy/paste text here

            } else {
                // no paste text change
                if (s.toString().contains("\n")) {
                    String text = s.toString();
                    text = text.replace("\n", "");
                    while (text.contains("  ")) {
                        text = text.replace("  ", " ");
                    }
                    if (text.length() > 1) {
                        s.clear();
                        if (!onEnterPressed(text)) {
                            s.append(text);
                        }
                    } else {
                        s.clear();
                        s.append(text);
                    }
                }
            }
            if (mChipsListener != null) {
                mChipsListener.onTextChanged(s);
            }
        }
    }

    private class KeyInterceptingInputConnection extends InputConnectionWrapper {

        public KeyInterceptingInputConnection(InputConnection target) {
            super(target, true);
        }

        @Override
        public boolean commitText(CharSequence text, int newCursorPosition) {
            return super.commitText(text, newCursorPosition);
        }

        @Override
        public boolean sendKeyEvent(KeyEvent event) {
            if (mEditText.length() == 0) {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
                        selectOrDeleteLastChip();
                        return true;
                    }
                }
            }
            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
                mEditText.append("\n");
                return true;
            }

            return super.sendKeyEvent(event);
        }

        @Override
        public boolean deleteSurroundingText(int beforeLength, int afterLength) {
            if (mEditText.length() == 0 && beforeLength == 1 && afterLength == 0) {
                return sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
                        && sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL));
            }

            return super.deleteSurroundingText(beforeLength, afterLength);
        }
    }

    public class Chip implements OnClickListener {

        private static final int MAX_LABEL_LENGTH = 30;

        private String mLabel;
        private final Uri mPhotoUri;
        private final Contact mContact;
        private final boolean mIsIndelible;
        private RelativeLayout mView;
        private View mIconWrapper;
        private TextView mTextView;
        private ImageView mCloseIcon;
        private boolean mIsSelected = false;

        public Chip(String label, Uri photoUri, Contact contact) {
            this(label, photoUri, contact, false);
        }

        public Chip(String label, Uri photoUri, Contact contact, boolean isIndelible) {
            this.mLabel = label;
            this.mPhotoUri = photoUri;
            this.mContact = contact;
            this.mIsIndelible = isIndelible;

            if (mLabel == null) {
                mLabel = contact.getEmailAddress();
            }

            if (mLabel.length() > MAX_LABEL_LENGTH) {
                mLabel = mLabel.substring(0, MAX_LABEL_LENGTH) + "...";
            }
        }

        public View getView() {
            if (mView == null) {
                mView = (RelativeLayout) inflate(getContext(), R.layout.chips_view, null);

                // Layout Params + margins
                RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
                        ViewGroup.LayoutParams.WRAP_CONTENT, (int) (CHIP_HEIGHT * mDensity));
                layoutParams.setMargins(0, 0, mChipsMargin, 0);
                mView.setLayoutParams(layoutParams);

                mIconWrapper = mView.findViewById(R.id.rl_ch_avatar);
                mTextView = (TextView) mView.findViewById(R.id.tv_ch_name);
                mCloseIcon = (ImageView) mView.findViewById(R.id.iv_ch_close);

                if (mTypeface != null) {
                    mTextView.setTypeface(mTypeface);
                }
                mView.setBackgroundResource(mChipsBgRes);
                if (mIsIndelible) {
                    ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColorIndelible);
                } else {
                    ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColor);
                }
                mIconWrapper.setBackgroundResource(R.drawable.ic_chip_delete_circle);
                if (mIsIndelible) {
                    mTextView.setTextColor(mChipsTextColorIndelible);
                } else {
                    mTextView.setTextColor(mChipsTextColor);
                }

                mView.setOnClickListener(this);
                mIconWrapper.setOnClickListener(this);
            }

            mCloseIcon.setBackgroundResource(mChipsDeleteBtnResId);

            updateViews();
            return mView;
        }

        private void updateViews() {
            mTextView.setText(mLabel);
            if (mTypeface != null) {
                mTextView.setTypeface(mTypeface);
            }

            if (isSelected()) {
                ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColorClicked);
                mTextView.setTextColor(mChipsTextColorClicked);
                mIconWrapper.getBackground().setColorFilter(mChipsDeleteBtnBgColorClicked,
                        PorterDuff.Mode.SRC_ATOP);
            } else {
                if (mIsIndelible) {
                    ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColorIndelible);
                    mTextView.setTextColor(mChipsTextColorIndelible);
                } else {
                    ((GradientDrawable) mView.getBackground()).setColor(mChipsBgColor);
                    mTextView.setTextColor(mChipsTextColor);
                    mIconWrapper.getBackground().setColorFilter(mChipsDeleteBtnBgColor, PorterDuff.Mode.SRC_ATOP);
                }

            }
        }

        @NonNull
        private String getInitials() {
            if (mLabel != null) {
                if (mLabel.trim().contains(" ")) {
                    String[] split = mLabel.trim().split(" ");
                    return String.format("%s%s", String.valueOf(split[0].charAt(0)),
                            String.valueOf(split[split.length - 1].charAt(0)));
                } else {
                    return String.valueOf(mLabel.charAt(0));
                }
            } else {
                return "";
            }
        }

        @Override
        public void onClick(View v) {
            mEditText.clearFocus();
            if (v.getId() == mView.getId()) {
                onChipInteraction(this, true);
            } else {
                onChipInteraction(this, false);
            }
        }

        public boolean isSelected() {
            return mIsSelected;
        }

        public void setSelected(boolean isSelected) {
            if (mIsIndelible) {
                return;
            }
            this.mIsSelected = isSelected;
        }

        public Contact getContact() {
            return mContact;
        }

        @Override
        public boolean equals(Object o) {
            if (mContact != null && o instanceof Contact) {
                return mContact.equals(o);
            }
            return super.equals(o);
        }

        @Override
        public String toString() {
            return "{" + "[Contact: " + mContact + "]" + "[Label: " + mLabel + "]" + "[PhotoUri: " + mPhotoUri + "]"
                    + "[IsIndelible" + mIsIndelible + "]" + "}";
        }
    }

    public interface ChipsListener {
        void onChipAdded(Chip chip);

        void onChipDeleted(Chip chip);

        void onTextChanged(CharSequence text);

        /**
         * return true to delete the invalid text.
         */
        boolean onInputNotRecognized(String text);
    }

    public static abstract class ChipValidator {
        public abstract boolean isValid(Contact contact);
    }
    //</editor-fold>
}