3

This is a problem I've been experiencing for quite some time and have already reached out about — other Android programmers found it intriguing.

Context: I have a custom EditText view that relies on XML for instantiation. I have two methods to create this view: either it is within the Layout file of the current Activity (1) or I dynamically add it to the layout at runtime using an Inflater (2). With both methods, the object is defined exactly the same in XML. However, there is a behavior with one that doesn't show in the other.

Behaviors: When using the first method, I get the correct and expected behavior, which is the following: The user types in the center of the EditText (restricted to one horizontal line). Once the user has typed enough to exceed the bounds of the EditText, the EditText will wrap_content to accommodate. When it changes width, the change is anchored to the middle of the view. Length is thus added to both the right AND the left of the box. When using the second method, I get the incorrect behavior: the width change is anchored to the left! Length is added to the right only.

Issue: I'd like to see the correct behavior with the second method and understand why the behavior could be different even though the XML is the same.

What I've Done: I've already reached out for help on the Android Studio Discord and have tried to tweak different aspects of the shared XML. The last thing I've done is to verify this problem still occurs with regular EditText and that my custom class isn't to blame.

Code Snippets: the shared XML (in activity layout OR "tag.xml")

<com.lab.guy.opener.TagView
    xmlns:app="http://schemas.android.com/apk/res-auto"

    style="@style/TagStyle"

    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintRight_toRightOf="parent" />

And "TagStyle":

 <style name="TagStyle">
    <item name="android:textSize">14sp</item>
    <item name="android:layout_height">21sp</item>
    <item name="android:layout_width">wrap_content</item>
    <item name="android:gravity">center</item>

    <item name="android:paddingLeft">10dp</item>
    <item name="android:paddingRight">10dp</item>

    <item name="android:focusableInTouchMode">true</item>
    <item name="android:maxLines">1</item>
    <item name="android:lines">1</item>
    <item name="android:singleLine">true</item>
    <item name="android:maxLength">20</item>
    <item name="android:inputType">textNoSuggestions|textVisiblePassword</item>
    <item name="android:imeOptions">actionDone</item>

    <item name="android:hint">@string/tag</item>
    <item name="android:textColor">@color/tag</item>
    <item name="android:textCursorDrawable">@drawable/tag_cursor</item>
    <item name="android:background">@drawable/tag_background</item>
</style>

The main layout is a ConstraintLayout.

If you need any other resources for testing, please do tell!

The only code I'm not willing to share is from TagView.java, since the problem occurs with standard EditTexts too.

Thank you very much from reading this far!

Edit: I've tried out more things to do, such as cross-testing on other devices and trying out other layout types (under Linear or Relative, I get the incorrect behavior regardless of which method I use). Currently, I'm trying to recreate the correct behavior on a default EditText that uses the second instantiation method (this time, trying out different XML).

Guy
  • 51
  • 5

2 Answers2

0

I have found a non-satisfactory workaround which nullifies the problem, and I'll share it for anyone who might experience this in the future. The initial question, however, remains unanswered.

So: if ever you need to instantiate a text-centered content-wrapping EditText through an Inflater and do not want width to be added to the right only, you must create a custom EditText child class equipped with a TextWatcher.

protected static int widthDiffThreshold = 4; //todo: verify

protected String rem;
protected String add;
protected int remLength;
protected int addLength;
protected Rect remBounds = new Rect();
protected Rect addBounds = new Rect();
protected Rect curBounds = new Rect();


private class SimpleTextListener implements TextWatcher{
    @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        getPaint().getTextBounds(s.toString(), 0, s.length(), curBounds);

        if(((getWidth() - getResources().getDimension(R.dimen.myPadding)*2) - curBounds.width()) <= widthDiffThreshold){
            rem = s.subSequence(start, start + count).toString();
        } else rem = null;
    }
    @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
        if(rem!=null) {
            add = s.subSequence(start, start + count).toString();

            getPaint().getTextBounds(rem, 0, rem.length(), remBounds);
            getPaint().getTextBounds(add, 0, add.length(), addBounds);

            remLength = -remBounds.width(); //todo: simplify
            addLength = addBounds.width();

            setX(getX() - ((float)remLength + (float)addLength) /2);
        }
    }

    @Override
    public void afterTextChanged(Editable s) {
        //some code
    }
}

What this code is effectively doing is the following: whenever the text is changed, we first check if the width difference between the text content and the containing box is low enough to warrant an automatic width-change (handled via the wrap-content attribute). To know this, I've measured a somewhat approximate threshold, but I invite you to do your own research to verify this value is appropriate.

If your view has padding, you should also take it into account here.

If a width-change will indeed occur, we now store the string that is going to be modified as well as the the string which will replace the first. This method ensures that selecting&copy-pasting does not break the system. In normal typing, the "rem" string will be empty when adding a letter and the "add" string will be empty when removing one.

Now, we measure both of those strings in real units, subtract the removed length from the added and move the view by half of the total. I've kept some variables which can be removed for clarity.

This, thankfully, seems to work very well. Some adjustments might be necessary and I'll update this answer whenever I discover any bugs.

Good luck out there!

Guy
  • 51
  • 5
0

I've at long last found the actual cause of and solution to the original issue!
Not that I fully understand it, but here goes:

private ConstraintSet myConstraintSet = new ConstraintSet();

...

CustomEditText myCustomView = new CustomEditText(new ContextThemeWrapper(this, R.style.CustomStyle), null, 0);
myCustomView.setId(View.generateViewId()); 
myConstraintLayout.addView(tag);

myConstraintSet.clone(myConstraintLayout);

myConstraintSet.connect(myCustomView.getId(), ConstraintSet.START, myConstraintLayout.getId(), ConstraintSet.START);
myConstraintSet.connect(myCustomView.getId(), ConstraintSet.END, myConstraintLayout.getId(), ConstraintSet.END);

myConstraintSet.applyTo(myConstraintLayout);


There are two things to note upfront about this code: firstly, I switched the instantiation method from using an Inflater to using ContextThemeWrapper. Feel free to use whichever works best for you, from what I've seen it doesn't seem to change much. Secondly, View.generateViewId() is a method which won't work for API <= 17. If this is an inconvenience for you as it was for me, there are some easy-to-integrate workarounds.

So, as you might guess, it seems the reason why the behaviors were different lies in whether or not the view we wish to instantiate is correctly constrained in the ConstraintLayout. It also seems that for this to work as we want, we NEED to use a ConstraintLayout. The necessary constraints are left-to-left and right-to-right relative to the parent (perhaps other objects can also work).

This solution may seem obvious, but I was mislead by the thought that setting those constraints in tag.xml would correctly apply them once instantiated. It... doesn't. I'm very surprised I never thought to play with the constraints of the original view, and am happy I did — though by complete accident.

This solution requires no XML other than styles.xml.

My previous solution can now be thrown out of the window completely. There are some inconveniences with this solution, such as having to clone the layout every time we want to set new constraints, the object behaving weirdly if proper IDs aren't given, the higher amount of code overall within the Activity and most egregious of all, the setX command now being relative to the horizontal center of the screen rather than absolute. I'll update this answer if I figure out how to fix that.

If there are other major problems on your end that I didn't experience, my other solution can still be used as a workaround. I'll update this answer if I have any more remarks or problems with it. Be sure to contact me if you have any other questions!



Oh, and lastly, if you have a deeper understanding of why I experience the problem at all when no constraints are set, be sure to post an answer. It would be interesting, insightful and probably be marked as the final correct answer.

Guy
  • 51
  • 5