I have made a class that is responsible for monitoring an EditText widget following the Observer pattern. Its sole function is to disable or re-enable auto-correct based on a method call. I am able to successfully achieve this, but the problem is that the new InputType only applies to new text I add to the EditText - old text still retains the red underline to show that it can be auto-corrected.
Is there any way I can force the new InputType to apply to the entire EditText block, and not simply the new content I add? I tried calling editText.invalidate() and editText.refreshDrawableState() in the hope all the text would refresh, but to no avail.
final class SpellCheckerObserver implements EditTextObserver {
public static final int KEY = KeyGenerator.generateUniqueId();
private int defaultInputType;
SpellCheckerObserver(EditTextSubject subject) {
subject.attach(SpellCheckerObserver.KEY, this);
}
#Override
public void activating(EditText editText) {
defaultInputType = editText.getInputType();
editText.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
}
#Override
public void deactivating(EditText editText) {
editText.setInputType(defaultInputType);
}
}
I found out the answer whilst looking through the source code for TextView, where I came across the removeSuggestionSpans() method.I wasn't aware that the suggestions were in fact a type of span, (unsurprisingly, the SuggestionSpan)
This meant I was able to remove the red underline with the following code:
SuggestionSpan[] spans = editText.getText().getSpans(
0, editText.length(), SuggestionSpan.class
);
if (spans.length > 0) {
for (SuggestionSpan span : spans) {
editText.getText().removeSpan(span);
}
}
Related
I'm developing an IME for Android, which includes some highlighting of the text input by the user with a background colour (using SpannableString).
When the device is rotated and the keyboard is redrawn, the text remains in the input box as entered by the user, however, all styling (i.e. background colour) is lost.
Any ideas why this might be happening and how to circumvent it?
I have found a solution, which is not perfect, but it works.
Set up an ExtractedText variable in the IME service class.
In the OnConfigurationChanged method, get the extracted text. Then in the onStartInputView method, delete the text in the bound editor, and replace it with the extracted text.
#Override
public void onConfigurationChanged (Configuration newConfig)
{
//get what's been input so far
ExtractedTextRequest req = new ExtractedTextRequest();
req.token = 0;
req.flags = InputConnection.GET_TEXT_WITH_STYLES;
extractedText = ic.getExtractedText(req, 0);
super.onConfigurationChanged(newConfig);
}
#Override public void onStartInputView(EditorInfo attribute, boolean restarting) {
...
if(extractedText!=null)
{
if(ic.deleteSurroundingText(9999, 0))
{
ic.commitText(extractedText.text, 1);
Log.i("onStartInputView", "Text Replaced");
}
else
{
Log.i("onStartInputView", "IC not valid");
}
}
super.onStartInputView(attribute, restarting);
}
I would need a way to detect if the EditText has been changed by the user typing something or by the app changing the text programmatically. Any standard way of doing this? I guess I could always do something hackish like unsetting the TextWatcher before setText() and setting it back again afterwards, but there's got to be a better way of doing this... right?
I tried checking if the EditText is focused in the TextWatcher, but that was of little help since the EditTexts gets focused "semi-randomly" anyway when scrolling...
Background
I have a ListView with EditTexts in every listitem. I've sorted out the basic problem of storing the values for the EditTexts for reuse when the user scrolls.
I also have a TextWatcher that sums up the values in all EditTexts and displays the sum when the user edits the content of any of the EditTexts.
The problem is that when I'm scrolling the list and my custom adapter is reentering the stored values in the EditTexts on bindView(), that also triggers the TextWatchers afterTextChanged() method, causing the scrolling to lag because the summing-up-function is triggered.
This sorted itself out a long time ago, but for anyone who finds their way here looking for an answer, here's what I did:
I ended up setting the Tag of the EditText to some arbitrary value right before I'm about to change it programmatically, and changing the value, and then resetting the Tag to null. Then in my TextWatcher.afterTextChanged() method I check if the Tag is null or not to determine if it was the user or the program that changed the value. Works like a charm!
Something like this:
edit.setTag( "arbitrary value" );
edit.setText( "My Text Value" );
edit.setTag(null);
and then
public void afterTextChanged(Editable s) {
if( view.getTag() == null )
// Value changed by user
else
// Value changed by program
}
The accepted answer is perfectly valid, but I have another approach;
#Override
public void onTextChanged(CharSequence charSequence,
int start, int before, int count) {
boolean userChange = Math.abs(count - before) == 1;
if (userChange) {
}
}
It works by checking if the change was a single character.
This is not a fool-proof solution as copy-paste operations might be missed, and non-user changes of a single character will also be missed.
Depending on your use case, this might be a viable solution.
One thing that helped to me is having boolean canListenInput field. Use it inside of watcher.
email.addTextChangedListener(new TextWatcher() {
#Override
public void afterTextChanged(Editable s) {
if (canListenInput) {
emailChanged = true;
}
}
});
Clear it before changing text programmatically. Set it inside of onAttachedToWindow, (after state) restoration:
#Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
canListenInput = true;
}
Depending on your use case (e.g. you are auto-populating this field when the user types into another field), you can also check if the view has focus, e.g.:
textView.doAfterTextChanged {
val isValueChangedByUser = textView.hasFocus()
// ...
}
I have created some extension methods to tackle this scenario
inline fun TextView.runTaggingCode(block: () -> Unit) {
this.setTag(R.string.tag_text_id, "set_from_code")
block()
this.setTag(R.string.tag_text_id, null)
}
fun TextView.isTaggedForCode() = this.getTag(R.string.tag_text_id) != null
where I have defined the R.string.tag_text_id as below
<string name="tag_text_id" translatable="false">dummy</string>
Now where I to use these methods, I will simply change my code as below,
override fun beforeTextChanged(
s: CharSequence, start: Int, count: Int,
after: Int,
) {
if (textView.isTaggedForCode()) {
return
}
textView.runTaggingCode {
// your logic here
}
}
But in case you don't want to change the same text view text, in it own TextWatcher you can also see the answer
You can do this by adding:
private String current = "";
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if(!s.toString().equals(current)){
[your_edittext].removeTextChangedListener(this);
//Format your string here...
current = formatted;
[your_edittext].setText(formatted);
[your_edittext].setSelection(formatted.length());
[your_edittext].addTextChangedListener(this);
}
Why links in ListView are lost, when scrolling? From debugging it's clear, that spans are not added second time on a TextView from the convertView.
Here's a piece of code which is called from adapter's getView.
...
String body = MyItemDetails.getBody(); // String to linkify
final Spannable spannable = MyCustomUri.addHashtagSpans(context, body);
viewHolder.textView.setText(spannable);
viewHolder.textView.setTextIsSelectable(true); // adds additional spans
viewHolder.textView.setMovementMethod(ArrowKeyMovementMethod.getInstance());
viewHolder.textView.setAutoLinkMask(Linkify.WEB_URLS);
...
MyCustomUri.addHashtagSpans() creates a SpannableString with MyCustomSpan with extends URLSpan.
Problem is that when I scroll up and down in the ListView links are lost. Whereas when screen is opened 1st time it's set correctly.
Now I made a dirty fix by disabling reuse of convertView :( Any ideas how to solve this problem better?
Some of the spannable information is likely being lost when the textview's data is written to a parcel for retention.
See TextView.onSaveInstanceState(), TextView.onRestoreInstanceState(), and TextView.SavedState.
It can often be very frustrating to determine what android will and will not retain. I often just setSaveEnabled(false) on my views to disable the unpredictable default behaviours of the base widgets.
Also, the viewholder pattern is only really intended for retaining view/layout instance hierarchies. To save you from having to inflate or find your views every getView(). It's always your responsibility to update a view's data when presenting it from getView().
You don't need to completely disable the viewholder pattern, instead just simply update the text every getView(), as you may already be doing.
Hello Use this custom class
public class MyCustomSpannable extends ClickableSpan {
String Url;
Context mContext;
public MyCustomSpannable(String Url, Context context) {
this.Url = Url;
mContext = context;
}
#Override
public void updateDrawState(TextPaint ds) {
// Customize your Text Look if required
ds.setColor(mContext.getResources().getColor(R.color.red_text));
ds.setFakeBoldText(true);
// ds.setStrikeThruText(true);
ds.setTypeface(CommonFunctios.getfontNormal(mContext));
// ds.setUnderlineText(true);
// ds.setShadowLayer(10, 1, 1, Color.WHITE);
// ds.setTextSize(15);
}
#Override
public void onClick(View widget) {
}
public String getUrl() {
return Url;
}
}
and in adapter replace your code with this
String text = holder.txt_terms.getText().toString();
SpannableStringBuilder stringBuilder = new SpannableStringBuilder(text);
MyCustomSpannable customSpannable = new MyCustomSpannable(text,
mcontext) {
#Override
public void onClick(View widget) {
Log.e("on click", "message");
((OpticalOffersActivity) mcontext).callDialogBox(position);
}
};
stringBuilder.setSpan(customSpannable, 0, text.length(),
Spannable.SPAN_INCLUSIVE_INCLUSIVE);
holder.txt_terms.setText(stringBuilder, BufferType.SPANNABLE.SPANNABLE);
holder.txt_terms.setMovementMethod(LinkMovementMethod.getInstance());
Hope it will help you.
if(convertView==null)
{
convertView.setTag(holder);
}
else
{
holder = (ViewHolder)convertView.getTag();
}
...
String body = MyItemDetails.getBody(); // String to linkify
final Spannable spannable = MyCustomUri.addHashtagSpans(context, body);
viewHolder.textView.setText(spannable);
viewHolder.textView.setTextIsSelectable(true); // adds additional spans
viewHolder.textView.setMovementMethod(ArrowKeyMovementMethod.getInstance());
viewHolder.textView.setAutoLinkMask(Linkify.WEB_URLS);
...
That spannable code must be placed outside the if-else loop in the getView() method, like the way I did it in the above code.
There are a couple problems at play here, so let me address them one at a time. The issues you've asked about directly (links disappearing) is a side effect of the fact that the auto linking behavior in TextView doesn't necessarily work that well when you are also adding your own spans to the text manually...best not to use it. Remove the setAutoLinkMask() trigger and the disappearing links issue will go away.
Instead, you can easily add the same web linking behavior directly into your text span with Linkify. However, this is only part of your problem. The MovementMethod you have chosen isn't really compatible with clickable links. The reason it (partially) works in your code now is because the auto link mask is causing the MovementMethod of the view to be secretly massaged under the hood to a LinkMovementMethod...which then gets reset after the view is recycled. A pattern I typically use (applied to your code example) would be:
final Spannable spannable = MyCustomUri.addHashtagSpans(context, body);
Linkify.addLinks(spannable, Linkify.WEB_URLS);
viewHolder.textView.setText(spannable);
addLinkMovementMethod(textView);
Where addLinkMovementMethod() is a helper I have that looks like this:
private void addLinkMovementMethod(TextView t) {
MovementMethod m = t.getMovementMethod();
if ((m == null) || !(m instanceof LinkMovementMethod)) {
if (t.getLinksClickable()) {
t.setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
This simply keeps from resetting the value on each view recycle if it isn't necessary. The previous code block will give you links that click properly and never disappear...
However, I'm guessing from the methods you've called that you are also attempting to make the linked text in the list selectable (e.g. calling setTextIsSelectable() and choosing the ArrowKeyMovementMethod). This gets a little trickier because of the MovementMethod issue I discussed above. In order to create a MovementMethod that supports both link clicks and text selection, I'll direct you to this existing SO post on the subject which includes sample code on the customizations you need to make: Can a TextView be selectable AND contain links?
I build EditText dynamically. Among other things, I set 2 properties: hint(.setHint) and inputType(.setInputType). My problem: when I invoke setInputType, setHint has no effect: blank edittexts remain blank with no hint. Once I comment out setInputType, I see all hints. I need both input type and hint. What to do? My code:
private EditText buildTextBox(Property property)
{
EditText control = new EditText(this);
control.setInputType(getInputTypeByPropertyInputType(property.getType()));// android.text.InputType.
control.setHint(property.getDisplayName());
return control;
}
private int getInputTypeByPropertyInputType(String type)
{
if (type.equals("integer"))
{
return android.text.InputType.TYPE_CLASS_NUMBER;
}
else
{
return android.text.InputType.TYPE_CLASS_TEXT;
}
}
#Eugene
Ensure you call control.SetHint() just before you call the control.setGravity() and control.setInputType(); and it works for me verrry much!
column1 = new EditText(this);
column1.setId(i);
column1.setHint("Value");
column1.setInputType(InputType.TYPE_CLASS_NUMBER);
column1.setGravity(Gravity.RIGHT);
I agree with Eugene.
Remove the gravity(just don't use CENTER) and the hint texts will come back as normal.
Nice find!
I have an Activity that extends TextWatcher to detect changes in certain EditTexts, so it implements:
public void afterTextChanged(Editable s)
My question is: If there are several EditTexts with .addTextChangedListener(this) set, how can I differentiate which one changed given the Editable object in the afterTextChanged procedure?
Another option, with fewer anonymous inner classes, would be to simply inspect the currently focused View. If your application for TextWatcher hinges solely on changes made by the user while typing, then the changes will always occur in the View that has current focus. Calling getCurrentFocus() from anywhere inside of an Activity or Window will return the View the user is focused on. From inside a TextWatcher, this will almost assuredly be the specific EditText instance.
SDK Docs Link
Hope that Helps!
There's one method to implement this without creating a TextWatcher object for every EditText, but I wouldn't use it:
protected void onCreate(Bundle savedInstanceState) {
// initialization...
EditText edit1 = findViewById(R.id.edit1);
edit1.addTextChangedListener(this);
EditText edit2 = findViewById(R.id.edit1);
edit2.addTextChangedListener(this);
}
private static CharSequence makeInitialString(EditText edit) {
SpannableStringBuilder builder = new SpannableStringBuilder();
builder.setSpan(edit, 0, 0, Spanned.SPAN_MARK_MARK);
return builder;
}
public void afterTextChanged(Editable s) {
EditText[] edits = s.getSpans( 0, s.length(), EditText.class );
if (edits.length != 1) {
// this mustn't happen
}
// here's changed EditText
EditText edit = edits[0];
}
see TextWatcher for more than one EditText basically create your own class to handle the listener with a constructor that defines the edittext you are monitoring and pass the edittext you are assigning.