package net.avenwu.support.widget; import android.content.Context; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RoundRectShape; import android.os.Build; import android.text.Editable; import android.text.InputFilter; import android.text.TextUtils; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.SparseIntArray; import android.util.TypedValue; import android.util.Xml; import android.view.Gravity; import android.view.InflateException; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; /** * Created by chaobin on 1/14/15. */ public class TagFlowLayout extends ViewGroup implements TextWatcher, View.OnKeyListener, View.OnClickListener { private static final int INVALID_VALUE = -1; private SparseIntArray mCachedPosition = new SparseIntArray(); private EditText mInputView; /** * Comma in both English & Chinese */ private char[] mKeyChar = new char[]{',', ','}; /** * Key event of enter, comma */ private int[] mKeyCode = new int[]{ KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_NUMPAD_COMMA }; private int mCheckIndex = INVALID_VALUE; private Decorator mDecorator; public TagFlowLayout(Context context) { this(context, null); } public TagFlowLayout(Context context, AttributeSet attrs) { super(context, attrs); mInputView = new EditText(context); mInputView.setBackgroundColor(Color.TRANSPARENT); mInputView.addTextChangedListener(this); mInputView.setOnKeyListener(this); mInputView.setPadding(0, 0, 0, 0); mInputView.setMinWidth(20); mInputView.setSingleLine(); mInputView.setGravity(Gravity.CENTER_VERTICAL); setDecorator(new SimpleDecorator(context)); addView(mInputView); setOnClickListener(this); previewInEditMode(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int maxWidth = 0; int maxHeight = 0; int width = 0; int height = 0; mCachedPosition.clear(); final int widthSpace = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); final int count = getChildCount(); int verticalCount = 0; for (int i = 0; i < count; ++i) { final View child = getChildAt(i); if (nullChildView(child)) { continue; } measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidthSpace = lp.leftMargin + child.getMeasuredWidth() + lp.rightMargin; int childHeightSpace = lp.topMargin + child.getMeasuredHeight() + lp.bottomMargin; if (maxWidth + childWidthSpace > widthSpace) { maxWidth = childWidthSpace; maxHeight = Math.max(maxHeight, childHeightSpace); height += maxHeight; mCachedPosition.put(i, ++verticalCount); } else { maxWidth += childWidthSpace; maxHeight = childHeightSpace; } width = Math.max(width, maxWidth); } height += maxHeight + getPaddingTop() + getPaddingBottom(); setMeasuredDimension(getImprovedSize(widthMeasureSpec, width), getImprovedSize(heightMeasureSpec, height)); } private boolean nullChildView(View child) { return child == null || child.getVisibility() == GONE; } private int getImprovedSize(int measureSpec, int size) { int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: case MeasureSpec.AT_MOST: return size; case MeasureSpec.EXACTLY: return specSize; } return specSize; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childTop = getPaddingTop(); int childLeft = getPaddingLeft(); final int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); if (nullChildView(child)) { continue; } final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); if (mCachedPosition.get(i, INVALID_VALUE) != INVALID_VALUE) { childTop += lp.topMargin + childHeight + lp.bottomMargin; childLeft = getPaddingLeft(); } else if (childTop == getPaddingTop()) { childTop += lp.topMargin; } childLeft += lp.leftMargin; setChildFrame(child, childLeft, childTop, childWidth, childHeight); childLeft += childWidth + lp.rightMargin; } } private void setChildFrame(View child, int left, int top, int width, int height) { child.layout(left, top, left + width, top + height); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { MarginLayoutParams params = new MarginLayoutParams(p); if (mDecorator != null) { params.setMargins(mDecorator.getMargin()[0], mDecorator.getMargin()[1], mDecorator.getMargin()[2], mDecorator.getMargin()[3]); } return params; } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { MarginLayoutParams params = new MarginLayoutParams(getContext(), attrs); if (mDecorator != null) { params.setMargins(mDecorator.getMargin()[0], mDecorator.getMargin()[1], mDecorator.getMargin()[2], mDecorator.getMargin()[3]); } return params; } @Override protected LayoutParams generateDefaultLayoutParams() { MarginLayoutParams params = new MarginLayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, mDecorator != null ? mDecorator.getHeight() : ViewGroup.LayoutParams.WRAP_CONTENT); if (mDecorator != null) { params.setMargins(mDecorator.getMargin()[0], mDecorator.getMargin()[1], mDecorator.getMargin()[2], mDecorator.getMargin()[3]); } return params; } /** * set the keyCode list which trigger the TAG generation, the default keyCode are * {@link android.view.KeyEvent#KEYCODE_ENTER},{@link android.view.KeyEvent#KEYCODE_COMMA} * * @param keyChar comma on mobile are not always keyCode, null will be ignored * @param keyCode keyCode defined in {@link android.view.KeyEvent}, null will be ignored */ public void setActionKeyCode(char[] keyChar, int... keyCode) { if (keyChar != null && keyChar.length > 0) { mKeyChar = keyChar; } if (keyCode != null && keyCode.length > 0) { mKeyCode = keyCode; } } @Override public boolean onKey(View v, int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { if (isKeyCodeHit(keyCode)) { if (!TextUtils.isEmpty(mInputView.getText().toString())) { generateTag(mInputView.getText().toString()); } return true; } else if (KeyEvent.KEYCODE_DEL == keyCode) { if (TextUtils.isEmpty(mInputView.getText().toString())) { deleteTag(); return true; } } } return false; } private boolean isKeyCodeHit(int keyCode) { if (mKeyCode != null && mKeyCode.length > 0) { for (int key : mKeyCode) { if (key == keyCode) { return true; } } } return false; } private boolean isKeyCharHit(char keyChar) { if (mKeyChar != null && mKeyChar.length > 0) { for (char key : mKeyChar) { if (key == keyChar) { return true; } } } return 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) { } @Override public void afterTextChanged(Editable s) { if (s.length() > 0) { if (isKeyCharHit(s.charAt(0))) { mInputView.setText(""); } else if (isKeyCharHit(s.charAt(s.length() - 1))) { mInputView.setText(""); generateTag(s.subSequence(0, s.length() - 1) + ""); } } } @Override public void onClick(View v) { if (v instanceof TagFlowLayout) { mInputView.requestFocus(); InputMethodManager m = (InputMethodManager) getContext() .getSystemService(Context.INPUT_METHOD_SERVICE); m.showSoftInput(mInputView, InputMethodManager.SHOW_FORCED); // clear check status if (mCheckIndex != INVALID_VALUE) { updateCheckStatus(getChildAt(mCheckIndex), false); mCheckIndex = INVALID_VALUE; } return; } final int index = indexOfChild(v); // skip unnecessary setting if (index != mCheckIndex) { mCheckIndex = index; updateCheckStatus(v, true); for (int i = 0; i < index; i++) { updateCheckStatus(getChildAt(i), false); } //skip input box for (int i = index + 1; i < getChildCount() - 1; i++) { updateCheckStatus(getChildAt(i), false); } } } private void deleteTag() { if (getChildCount() > 1) { removeViewAt(mCheckIndex == INVALID_VALUE ? indexOfChild(mInputView) - 1 : mCheckIndex); mCheckIndex = INVALID_VALUE; mInputView.requestFocus(); } } /** * Auto generate the last input content as tag label */ public void autoComplete() { if (!TextUtils.isEmpty(mInputView.getText().toString())) { generateTag(null); } } /** * can be null */ private void generateTag(CharSequence tag) { CharSequence tagString = tag == null ? mInputView.getText().toString() : tag; mInputView.getText().clear(); final int targetIndex = indexOfChild(mInputView); TextView tagLabel; if (mDecorator.getLayout() != INVALID_VALUE) { View view = View.inflate(getContext(), mDecorator.getLayout(), null); if (view instanceof TextView) { tagLabel = (TextView) view; } else { throw new IllegalArgumentException( "The custom layout for tagLabel label must have TextView as root element"); } } else { tagLabel = new TextView(getContext()); tagLabel.setPadding(mDecorator.getPadding()[0], mDecorator.getPadding()[1], mDecorator.getPadding()[2], mDecorator.getPadding()[3]); tagLabel.setTextSize(mDecorator.getTextSize()); } updateCheckStatus(tagLabel, false); tagLabel.setText(tagString); tagLabel.setSingleLine(); tagLabel.setGravity(Gravity.CENTER_VERTICAL); tagLabel.setEllipsize(TextUtils.TruncateAt.END); if (mDecorator.getMaxLength() != INVALID_VALUE) { InputFilter maxLengthFilter = new InputFilter.LengthFilter(mDecorator.getMaxLength()); tagLabel.setFilters(new InputFilter[]{maxLengthFilter}); } tagLabel.setClickable(true); tagLabel.setOnClickListener(this); addView(tagLabel, targetIndex); mInputView.requestFocus(); } private void updateCheckStatus(View view, boolean checked) { if (view == null) { return; } // don't reuse drawable for different tag label Drawable[] drawables = mDecorator.getBackgroundDrawable(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { view.setBackgroundDrawable(drawables[drawables.length > 1 ? (checked ? 1 : 0) : 0]); } else { view.setBackground(drawables[drawables.length > 1 ? (checked ? 1 : 0) : 0]); } final int[] color = mDecorator.getTextColor(); if (color != null && color.length > 0) { ((TextView) view).setTextColor((color[color.length > 1 ? (checked ? 1 : 0) : 0])); } } private void previewInEditMode() { if (isInEditMode()) { generateTag("Hot Tag Ckecked"); generateTag("TAG A"); generateTag("TAG B"); generateTag("TAG C"); updateCheckStatus(getChildAt(0), true); mInputView.setText("Input here..."); } } public void setDecorator(Decorator decorator) { if (decorator != null) { mDecorator = decorator; try { setInnerAttribute(); } catch (Exception e) { e.printStackTrace(); throw new UnknownError(e.getMessage() + "\n unavailable to setDecorator"); } } } /** * Make sure the input EditView looks like TextView label */ private void setInnerAttribute() throws XmlPullParserException, IOException, ClassNotFoundException { mInputView.setTextSize(mDecorator.getTextSize()); if (mDecorator.getMaxLength() != INVALID_VALUE) { InputFilter maxLengthFilter = new InputFilter.LengthFilter(mDecorator.getMaxLength()); mInputView.setFilters(new InputFilter[]{maxLengthFilter}); } if (mDecorator.getTextColor() != null && mDecorator.getTextColor().length > 1) { mInputView.setTextColor(mDecorator.getTextColor()[0]); } if (mDecorator.getLayout() != INVALID_VALUE) { XmlResourceParser parser = getResources().getLayout(mDecorator.getLayout()); final AttributeSet set = Xml.asAttributeSet(parser); int type = 0; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } final String name = parser.getName(); if ("TextView".equals(name) || Class.forName(name).isInstance(TextView.class)) { int[] attr = new int[]{ android.R.attr.layout_width, android.R.attr.layout_height, }; TypedArray array = getContext().obtainStyledAttributes(set, attr); final int height = array.getDimensionPixelSize(1, 0); if (height != 0) { MarginLayoutParams layoutParams = (MarginLayoutParams) mInputView .getLayoutParams(); layoutParams.height = height; } //TODO other useful attribute array.recycle(); } else { throw new InflateException(parser.getPositionDescription() + ": Only TextView or subclass of TextView is supported!"); } } } public CharSequence[] getTagArray() { final int count = getChildCount(); if (count > 1) { CharSequence[] tags = new CharSequence[count - 1]; for (int i = 0; i < count - 1; i++) { View child = getChildAt(i); if (child instanceof TextView) { tags[i] = ((TextView) child).getText(); } } return tags; } return new CharSequence[]{}; } public void setTagArray(CharSequence... tags) { for (CharSequence tag : tags) { generateTag(tag); } } public void clearTags() { while (getChildCount() > 1) { removeAllViews(); addView(mInputView); } } public void setInputable(boolean enable) { mInputView.setVisibility(enable ? View.VISIBLE : View.GONE); mInputView.setEnabled(enable); } /** * Implements your own Decorator to custom the tag view */ interface Decorator { /** * Size in unit of sp */ public int getTextSize(); /** * Padding on mLeftView, top, right, bottom in unit of dip */ public int[] getPadding(); /** * Margin on mLeftView, top, right, bottom in unit of dip */ public int[] getMargin(); /** * Color in format of AARRGGBB */ public int[] getTextColor(); /** * Tag view's background can be satisfied either by color or drawable resources, */ public Drawable[] getBackgroundDrawable(); /** * Size in unit of dip, if you provide custom layout by {@link #getLayout()}, it must have * the same height value */ public int getHeight(); /** * Provide your own layout id so you can custom the tag view what ever you like, * keep in mind the layout must have TextView as root element * * @return -1 will be ignored */ public int getLayout(); /** * @return */ public int getMaxLength(); } /** * Default decorator which will set on the TAG view */ public static class SimpleDecorator implements Decorator { protected int textSize; protected int[] textColor; protected int[] padding; protected int[] margin; protected int mTagHeight; protected int mRadius; public SimpleDecorator(Context context) { textSize = 14; final int p = getPixelSize(context, TypedValue.COMPLEX_UNIT_DIP, 5); padding = new int[]{p, p, p, p}; final int m = getPixelSize(context, TypedValue.COMPLEX_UNIT_DIP, 2); margin = new int[]{m, m, m, m}; mRadius = getPixelSize(context, TypedValue.COMPLEX_UNIT_DIP, 5); mTagHeight = getPixelSize(context, TypedValue.COMPLEX_UNIT_DIP, 30); textColor = new int[]{0xFF000000, 0xFFFFFFFF}; } private int getPixelSize(Context context, int unit, int size) { return (int) TypedValue .applyDimension(unit, size, context.getResources().getDisplayMetrics()); } @Override public int getTextSize() { return textSize; } @Override public int[] getPadding() { return padding; } @Override public int[] getMargin() { return margin; } @Override public int[] getTextColor() { return textColor; } public int getHeight() { return mTagHeight; } public Drawable[] getBackgroundDrawable() { return new Drawable[]{ newRoundRectShape(0xFF4285f4, mRadius), newRoundRectShape(0xFF3f51b5, mRadius) }; } @Override public int getLayout() { return INVALID_VALUE; } @Override public int getMaxLength() { return 20; } protected Drawable newRoundRectShape(int color, int radius) { ShapeDrawable shape = new ShapeDrawable(new RoundRectShape(new float[]{radius, radius, radius, radius, radius, radius, radius, radius}, null, null)); shape.getPaint().setStyle(Paint.Style.FILL); shape.getPaint().setColor(color); shape.getPaint().setAntiAlias(true); return shape; } } }