0

I have a MultiAutoCompleteTextView which presents a popup list when the user enters @ and a character.

When the user enters characters, the list will contain any of my test strings that matches the characters entered.

When the user selects an item from the list, i add a URLSpan to that MultiAutoCompleteTextView so the text starting with @ is underlined.

REQUIREMENT

If the user then edits that spanned text i want to remove that span.

The MultiAutoCompleteTextView will automatically reshow the selection so the user can choose a new value which will create the span again.

ISSUE

I can not work out how to remove the span when the user is editing the text.

I believe I need to use a text watcher, but i can't workout how to determine when a spanned text is the text being edited.

What I have tried results in the span being removed as soon as the user picks an item from the list and the span added so the text is changed. The text watcher then removes that span.

I need the text watcher to not remove the span, when the span is first added when the user selects an item from the list, but then remove the span if the user edits the text for the span.

CODE

MainActivity with TextWatcher

package com.example.test;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextWatcher;
import android.text.style.URLSpan;
import android.view.View;
import android.widget.Button;
import android.widget.MultiAutoCompleteTextView;
import android.widget.TextView;


public class MainActivity extends AppCompatActivity {

    MultiAutoCompleteTextView multiAutoCompleteTextView;

    Button getResultsBtn;
    Button clearBtn;

    TextView resultsTextViewText;
    TextView resultsTextViewSpans;

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

        multiAutoCompleteTextView = findViewById(R.id.test_text_view);

        getResultsBtn = findViewById(R.id.get_results_btn);
        clearBtn = findViewById(R.id.clear_btn);

        resultsTextViewText = findViewById(R.id.results_text_view_text);
        resultsTextViewSpans = findViewById(R.id.results_text_view_spans);


        SuggestionsAdapter adapter = new SuggestionsAdapter(this, android.R.layout.simple_list_item_1);
        multiAutoCompleteTextView.setAdapter(adapter);

        multiAutoCompleteTextView.setThreshold(1);

        multiAutoCompleteTextView.addTextChangedListener(new MyTextWatcher());

        // HAVE TO HAVE A TOKENIZER OTHERWISE IT DOES NOT WORK
        multiAutoCompleteTextView.setTokenizer(new SuggestionsTokenizer());

        clearBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                resetViews();
            }
        });

        getResultsBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // reset the text view's contents
                resetViews();

                Editable editable = multiAutoCompleteTextView.getText();
                String stringValue = editable.toString();

                // The textView's visible text
                resultsTextViewText.setText(stringValue);

                // info about the spans
                StringBuffer sb = new StringBuffer();
                URLSpan[] spans = editable.getSpans(0, editable.length(), URLSpan.class);
                if (spans != null && spans.length > 0) {
                    for (URLSpan span : spans) {
                        sb.append("URL:").append(span.getURL()).append("\n\n");
                    }
                    resultsTextViewSpans.setText(sb.toString());
                } else {
                    resultsTextViewSpans.setText("No spans");
                }

            }
        });


    }

    private void resetViews() {
        resultsTextViewText.setText("");
        resultsTextViewSpans.setText("");
    }

    class MyTextWatcher implements TextWatcher {

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            System.out.println("beforeTextChanged | s = " + s.toString() + " start = " + start + " count = " + count);
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            System.out.println("onTextChanged | s = " + s.toString() + " start = " + start + " before = " + before + " count = " + count);

            // determine if the text changed is WITHIN a span, if it is, remove that span
            Spannable spannable = multiAutoCompleteTextView.getText();


            int next;
            for (int i = 0; i < spannable.length(); i = next) {
                // find the next span transition
                next = spannable.nextSpanTransition(i, spannable.length(), URLSpan.class);

                if (start > i && start < next ) {
                    System.out.println("START IS IN THE RANGE");
                }
                // get all spans in this range
                URLSpan[] spans = spannable.getSpans(i, next, URLSpan.class);
                for (URLSpan span : spans){
                    System.out.println("spans = " + span.getURL());
                    spannable.removeSpan(span);
                }
            }
        }

        @Override
        public void afterTextChanged(Editable s) {
            System.out.println("afterTextChanged | s = " + s.toString());
        }
    }
}

Main Activity layout

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <MultiAutoCompleteTextView
        android:id="@+id/test_text_view"
        style="@style/my_suggestions_edittext"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:completionThreshold="1"
        android:hint="Test the auto complete text view"
        android:padding="4dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/clear_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Clear"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/test_text_view" />

    <Button
        android:id="@+id/get_results_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="48dp"
        android:text="Get Results"
        app:layout_constraintStart_toEndOf="@+id/clear_btn"
        app:layout_constraintTop_toBottomOf="@+id/test_text_view" />

    <!-- Text -->
    <TextView
        android:id="@+id/results_text_view_text_title"
        android:text="AutoCompleteTextView's TEXT"
        style="@style/my_suggestions_title"
        android:layout_marginTop="28dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/get_results_btn" />

    <TextView
        android:id="@+id/results_text_view_text"
        style="@style/my_suggestions_results"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/results_text_view_text_title" />

    <!-- Spans -->
    <TextView
        android:id="@+id/results_text_view_spans_title"
        android:text="AutoCompleteTextView's SPANS"
        android:layout_marginTop="28dp"
        style="@style/my_suggestions_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/results_text_view_text" />

    <TextView
        android:id="@+id/results_text_view_spans"
        style="@style/my_suggestions_results"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/results_text_view_spans_title" />

</androidx.constraintlayout.widget.ConstraintLayout>

Tokenizer

package com.example.test;

import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.MultiAutoCompleteTextView;

import java.util.regex.Pattern;

public class SuggestionsTokenizer implements MultiAutoCompleteTextView.Tokenizer  {

    boolean includeLogging = false;

    public static final Pattern TOKEN_CHARACTERS_PATTERN = Pattern.compile(
            "[\\p{Alnum}]", Pattern.CASE_INSENSITIVE);

    public static final char STARTING_CHAR = '@';

    // start of token is the @ character
    @Override
    public int findTokenStart(CharSequence text, int cursorPosition) {
        /*
        // TOKEN 1
        // THIS VERSION MEANS @A WILL SHOW THE LIST OF ALL VALUES, BUT WHEN SELECTING A VALUE
        // FROM THE LIST THE TEXTBOX CONTAINS TWO @@
        int i = cursorPosition;

        while (i > 0 && text.charAt(i - 1) != '@') {
            i--;
        }

        //Check if token really started with @, else we don't have a valid token
        if (i < 1 || text.charAt(i - 1) != '@') {
            return cursorPosition;
        }

        return i;

         */

        // TOKEN 2
        // THIS VERSION STOPS THE LIST SHOWING ANYTHING OTHER THAN ALL,
        // BUT THE TEXTBOX ONLY ENDS UP WITH ONE @

        int i = cursorPosition;
        boolean foundStartingChar = false;
        while (i > 0) {
            //System.out.println("findTokenStart | i = " + i);
            char inspectChar = text.charAt(i - 1);
            if (inspectChar == STARTING_CHAR) {
                if (includeLogging) System.out.println("findTokenStart | found Starting Char");
                foundStartingChar = true;
                i--;
                break;
            } else {
                // see if it is in the allowed set
                if (TOKEN_CHARACTERS_PATTERN.matcher(String.valueOf(text.charAt(i-1))).matches()) {
                    if (includeLogging) System.out.println("findTokenStart | Character is in the allowed set of characters");
                    i--;
                } else {
                    break;
                }
            }
        }

        // if the starting character was not found, return the current cursorPosition
        if (!foundStartingChar) {
            i = cursorPosition;
        }

        if (includeLogging) System.out.println("findTokenStart | returning " + i);

        return i;

    }

    // end of a token is when the text is NOT a letter or character from ANY language
    @Override
    public int findTokenEnd(CharSequence text, int cursor) {
        int i = cursor;
        int len = text.length();

        while (i < len) {
            System.out.println("Character{" + i + ") = " + text.charAt(i));
            if (TOKEN_CHARACTERS_PATTERN.matcher(String.valueOf(text.charAt(i))).matches()) {
                if (includeLogging) System.out.println("Character is in the allowed set of characters");
                i++;
            } else {
                if (includeLogging) System.out.println("Character is in NOT the allowed set of characters, i = ");
                return i;
            }
        }

        return len;
    }

    /*
    // End of a token is a space
    @Override
    public int findTokenEnd(CharSequence text, int cursor) {
        int i = cursor;
        int len = text.length();

        while (i < len) {
            if (text.charAt(i) == ' ') {
                return i;
            } else {
                i++;
            }
        }

        return len;
    }
    */

    // Returns text, modified, if necessary, to ensure that it ends with a token terminator
    // (i.e. a space ).
    public CharSequence terminateToken(CharSequence inputText) {
        int i = inputText.length();

        // work backwards from the end of the text to get to the first non-space character
        while (i > 0 && inputText.charAt(i - 1) == ' ') {
            i--;
        }

        if (i > 0 && inputText.charAt(i - 1) == ' ') {
            // text is just spaces
            return inputText;
        } else {
            /*
            // add a space to the end of the text
            if (inputText instanceof Spanned) {
                SpannableString sp = new SpannableString(inputText + " ");
                TextUtils.copySpansFrom((Spanned) inputText, 0, inputText.length(),
                        Object.class, sp, 0);
                return sp;
            } else {
                return inputText + " ";
            }
            */

            // return just the text without anything added to the end
            if (inputText instanceof Spanned) {
                SpannableString sp = new SpannableString(inputText);
                TextUtils.copySpansFrom((Spanned) inputText, 0, inputText.length(),
                                         Object.class, sp, 0);
                return sp;
            } else {
                return inputText;
            }
        }
    }
}

Adapter

package com.example.test;

import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.URLSpan;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import android.widget.Filterable;

import androidx.annotation.NonNull;

import org.apache.commons.lang3.StringUtils;

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

/*
 * NOTES:
 * When you hit delete at the end of the token Android is deleting all the text back to what you typed  https://stackoverflow.com/questions/76689412/multiautocompletetextview-deleting-end-character-from-selected-value-removes-all
 * Android is putting the underline on the text as you are typing
 */
public class SuggestionsAdapter  extends ArrayAdapter<String> implements Filterable {

    public static String[] suggestionValues = {"a", "ant", "ant.smith", "apple", "asp", "android", "animation", "adobe",
            "chrome", "chromium", "firefox", "freeware", "fedora"};


    public static final String ALL_URL_VALUE = "all_value";
    public static final String SPECIFIC_URL_PREFIX = "specific_value";
    public static final String RENDERING_MENTIONS_MEMBERS_LINK_TEMPLATE = SPECIFIC_URL_PREFIX + "=%s";
    /**
     * List of results.
     */
    private List<String> m_resultList;

    public SuggestionsAdapter(@NonNull Context context, int resource) {
        super(context, resource);
    }

    @Override
    public Filter getFilter() {
        return new Filter() {

            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                // WITH TOKEN 1, THE CONSTRAINT WILL NEVER HAVE THE @
                // WITH TOKEN 2, THE CONTSTRAINT MAY HAVE THE @

                FilterResults filterResults = new FilterResults();
                if (constraint != null) {


                    m_resultList = new ArrayList<>();

                    m_resultList.add("ALL"); // Special case@a

                    /*
                    // TOKEN 1
                    String searchTerm = constraint.toString();
                    for (String s : suggestionValues) {
                        if (StringUtils.startsWithIgnoreCase(s, searchTerm)) {
                            m_resultList.add(s);
                        }
                    }
                     */


                    // TOKEN 2
                    if (constraint.toString().length() > 1) {
                        String searchTerm = constraint.toString().substring(1);
                        for (String s : suggestionValues) {
                            if (StringUtils.startsWithIgnoreCase(s, searchTerm)) {
                                m_resultList.add(s);
                            }
                        }
                    }

                    // Assign the data to the FilterResults
                    filterResults.values = m_resultList;
                    filterResults.count = m_resultList.size();
                }
                return filterResults;
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                if (results != null && results.count > 0) {
                    notifyDataSetChanged();
                } else {
                    notifyDataSetInvalidated();
                }
            }

            // Called when a single value from the popup is selected
            // add a span for that specific string
            @Override
            public CharSequence convertResultToString(Object resultValue) {
                // returns just a string
                //return resultValue.toString();

                // return a URL Span

                String str = (String) resultValue;

                // Display string = @SelectedValue
                Spannable s = new SpannableString("@" + str);

                // Span = URL Span
                // - @ALL           : <a href="all_value">str</a>
                // - @specificValue : <a href="specific_value=str">str</a>
                URLSpan urlSpan = StringUtils.equals(str, "ALL")
                                ? new URLSpan(ALL_URL_VALUE)
                                : new URLSpan(String.format(
                                RENDERING_MENTIONS_MEMBERS_LINK_TEMPLATE, str));
                s.setSpan(urlSpan, 0, s.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

                return s;

            }
        };
    }


    @Override
    public int getCount() {
        return m_resultList.size();
    }


    @Override
    public String getItem(int index) {
        return (m_resultList != null) ? m_resultList.get(index) : null;
    }


}
se22as
  • 2,282
  • 5
  • 32
  • 54

0 Answers0