Creating a custom editText with tag-like feature
Asked Answered
L

3

31

I've been searching around but could not find any answers to this. What I'm trying to implement is an EditText similar to the 'To' field found in the composing screen of the ICS gmail app.

Here's an image describing what I want: enter image description here

I'm thinking of extending EditText and implementing my own custom EditText class but I'm not really sure how to do that or even if that's the best solution. Any thoughts?

Lark answered 3/7, 2012 at 20:19 Comment(4)
Off the cuff, I would guess that you are seeing a regular EditText, on a Spannable that contains ImageSpan elements for the "tags". However, if those "x" portions mean tapping on the tag removes it, then I don't think that is possible with ImageSpan.Guidon
Hmm that sounds reasonable but yeah, google got it to work (with the little "x" to remove it) on their gmail app so I'm pretty sure there's a full solution somewhere. Hopefully it's not too much of a hackLark
@BillX: May I know what solution you have decided to use? Thanks!Meraz
@LocHa Sorry about the late reply, it was a while ago :). But I think I went with something close to what I posted below. Another good resource that I found sometime after was kpbird.com/2013/02/android-chips-edittext-token-edittext.htmlLark
L
7

Hm, took a while to find a similar question but nonetheless, here's the closest answer I found. I knew other people had this kind of problem before! Thanks to CommonsWare for pointing me in the right direction.

Lark answered 5/7, 2012 at 20:19 Comment(0)
B
10

Adapted the solution from this answer. Separates the input automatically when inserting a comma (separator can be adjusted). Creates a an ImageSpan and a ClickableSpan (entries can be removed by clicking on the right part).

public class TagEditText extends EditText {

    TextWatcher textWatcher;

    String lastString;

    String separator = ",";

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


    private void init() {
        setMovementMethod(LinkMovementMethod.getInstance());

        textWatcher = new TextWatcher() {
            @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) {
                String thisString = s.toString();
                if (thisString.length() > 0 && !thisString.equals(lastString)) {
                    format();

                }
            }
        };

        addTextChangedListener(textWatcher);
    }


    private void format() {

        SpannableStringBuilder sb = new SpannableStringBuilder();
        String fullString = getText().toString();

        String[] strings = fullString.split(separator);


        for (int i = 0; i < strings.length; i++) {

            String string = strings[i];
            sb.append(string);

            if (fullString.charAt(fullString.length() - 1) != separator.charAt(0) && i == strings.length - 1) {
                break;
            }

            BitmapDrawable bd = (BitmapDrawable) convertViewToDrawable(createTokenView(string));
            bd.setBounds(0, 0, bd.getIntrinsicWidth(), bd.getIntrinsicHeight());

            int startIdx = sb.length() - (string.length());
            int endIdx = sb.length();

            sb.setSpan(new ImageSpan(bd), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

            MyClickableSpan myClickableSpan = new MyClickableSpan(startIdx, endIdx);
            sb.setSpan(myClickableSpan, Math.max(endIdx-2, startIdx), endIdx, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

            if (i < strings.length - 1) {
                sb.append(separator);
            } else if (fullString.charAt(fullString.length() - 1) == separator.charAt(0)) {
                sb.append(separator);
            }
        }


        lastString = sb.toString();

        setText(sb);
        setSelection(sb.length());

    }

    public View createTokenView(String text) {


        LinearLayout l = new LinearLayout(getContext());
        l.setOrientation(LinearLayout.HORIZONTAL);
        l.setBackgroundResource(R.drawable.bordered_rectangle_rounded_corners);

        TextView tv = new TextView(getContext());
        l.addView(tv);
        tv.setText(text);
        tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14);

        ImageView im = new ImageView(getContext());
        l.addView(im);
        im.setImageResource(R.drawable.ic_cross_15dp);
        im.setScaleType(ImageView.ScaleType.FIT_CENTER);

        return l;
    }

    public Object convertViewToDrawable(View view) {
        int spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        view.measure(spec, spec);
        view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());

        Bitmap b = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888);

        Canvas c = new Canvas(b);

        c.translate(-view.getScrollX(), -view.getScrollY());
        view.draw(c);
        view.setDrawingCacheEnabled(true);
        Bitmap cacheBmp = view.getDrawingCache();
        Bitmap viewBmp = cacheBmp.copy(Bitmap.Config.ARGB_8888, true);
        view.destroyDrawingCache();
        return new BitmapDrawable(getContext().getResources(), viewBmp);
    }

    private class MyClickableSpan extends ClickableSpan{

        int startIdx;
        int endIdx;

        public MyClickableSpan(int startIdx, int endIdx) {
            super();
            this.startIdx = startIdx;
            this.endIdx = endIdx;
        }

        @Override
        public void onClick(View widget) {



            String s = getText().toString();

            String s1 = s.substring(0, startIdx);
            String s2 = s.substring(Math.min(endIdx+1, s.length()-1), s.length() );

            TagEditText.this.setText(s1 + s2);
        }

    }
}

R.drawable.bordered_rectangle_rounded_corners:

<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid
        android:color="@color/transparent"/>
    <stroke android:width="1dp" android:color="#AAAAAA" />
    <corners
        android:radius="100dp" />
    <padding
        android:left="5dp"
        android:top="5dp"
        android:right="5dp"
        android:bottom="5dp" />
</shape>

Last thing to add is png for the "x-Button". Works well so far, only problem is that pressing long on the delete-key doesn't work (if someone has an idea how to make it work, feel free to comment)

Brigette answered 7/7, 2016 at 9:6 Comment(1)
Nice work there, I was wondering why can't I see a cursor blinking initially when I add this.Housewares
L
7

Hm, took a while to find a similar question but nonetheless, here's the closest answer I found. I knew other people had this kind of problem before! Thanks to CommonsWare for pointing me in the right direction.

Lark answered 5/7, 2012 at 20:19 Comment(0)
B
4

I couldn't find a good solution, so I would up building my own library to handle this: TokenAutoComplete. Here's a basic example:

public class ContactsCompletionView extends TokenCompleteTextView {
    public ContactsCompletionView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected View getViewForObject(Object object) {
        Person p = (Person)object;

        LayoutInflater l = (LayoutInflater)getContext().getSystemService(Activity.LAYOUT_INFLATER_SERVICE);
        LinearLayout view = (LinearLayout)l.inflate(R.layout.contact_token, (ViewGroup)ContactsCompletionView.this.getParent(), false);
        ((TextView)view.findViewById(R.id.name)).setText(p.getName());

        return view;
    }

    @Override
    protected Object defaultObject(String completionText) {
        //Stupid simple example of guessing if we have an email or not
        int index = completionText.indexOf('@');
        if (index == -1) {
            return new Person(completionText, completionText.replace(" ", "") + "@example.com");
        } else {
            return new Person(completionText.substring(0, index), completionText);
        }
    }
}

Layout code for contact_token (you'll need to find your own x drawable)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:background="@drawable/token_background">
    <TextView android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:textSize="14sp"
        android:text="Test Me"
        android:padding="2dp" />

    <ImageView
        android:layout_height="10dp"
        android:layout_width="10dp"
        android:src="@drawable/x"
        android:layout_gravity="center_vertical"
        android:layout_marginLeft="3dp"
        android:layout_marginRight="5dp" />
</LinearLayout>

Token backgound drawable

<shape xmlns:android="http://schemas.android.com/apk/res/android" >
    <solid android:color="#ffafafaf" />
    <corners
        android:topLeftRadius="5dp"
        android:bottomLeftRadius="5dp"
        android:topRightRadius="5dp"
        android:bottomRightRadius="5dp" />
</shape>

Person object code

public class Person implements Serializable {
    private String name;
    private String email;

    public Person(String n, String e) { name = n; email = e; }

    public String getName() { return name; }
    public String getEmail() { return email; }

    @Override
    public String toString() { return name; }
}

Sample activity

public class TokenActivity extends Activity {
    ContactsCompletionView completionView;
    Person[] people;
    ArrayAdapter<Person> adapter;

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

        people = new Person[]{
                new Person("Marshall Weir", "[email protected]"),
                new Person("Margaret Smith", "[email protected]"),
                new Person("Max Jordan", "[email protected]"),
                new Person("Meg Peterson", "[email protected]"),
                new Person("Amanda Johnson", "[email protected]"),
                new Person("Terry Anderson", "[email protected]")
        };

        adapter = new ArrayAdapter<Person>(this, android.R.layout.simple_list_item_1, people);

        completionView = (ContactsCompletionView)findViewById(R.id.searchView);
        completionView.setAdapter(adapter);
        completionView.setTokenClickStyle(TokenCompleteTextView.TokenClickStyle.Delete);
    }
}

Layout code

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.tokenautocomplete.ContactsCompletionView
        android:id="@+id/searchView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</RelativeLayout>
Besant answered 22/10, 2013 at 23:8 Comment(3)
I am in the same BillX situation. May I know your solution work with x-close button? Thanks.Meraz
@LocHa Did you find any solution for x-close button functionality?Whop
@SwapnilSonar: I didn't find any complete solution. I created my own way to edit tags (Not like the way BillX want). You can learn something from kpbird.com/2013/02/android-chips-edittext-token-edittext.htmlMeraz

© 2022 - 2024 — McMap. All rights reserved.