28

I'm trying to write a custom compound view composed by a TextView and an EditText, _compound_view.xml_:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/compoundText"
android:layout_width="match_parent"
android:layout_height="wrap_content" >

<TextView
    android:id="@+id/textLabel"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Label" />

<EditText
    android:id="@+id/textEdit"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="enter text here" >
</EditText>

and this is the class extending LinearLayout:

public class CompoundView extends LinearLayout {

public CompoundView(Context context, AttributeSet attrs) {
    super(context, attrs);

    readAttributes(context, attrs);
    init(context);
}

public CompoundView(Context context) {
    super(context);

    init(context);
}

private void init(Context c) {

    final LayoutInflater inflater = (LayoutInflater) c
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    inflater.inflate(R.layout.compound_view, this);

}
   }

Now, if I use 2 of these View in my _activity_main.xml_:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<it.moondroid.compoundview.example.CompoundView
    android:id="@+id/compoundview1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true" />

<it.moondroid.compoundview.example.CompoundView
    android:id="@+id/compoundview2"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_below="@id/compoundview1" />
</RelativeLayout>

and in the Activity code I only inflate the RelativeLayout, without managing onSaveInstanceState:

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

}

then when I write something in the 2nd EditText and I rotate my device, the same text appears in the EditText of the first custom View.

Why is happening this behaviour?

EDIT:

I solved the issue by removing android:id and using android:tag for the EditText in compound_view.xml, then managing the saving of the EditText state in CompoundView class:

@Override
protected Parcelable onSaveInstanceState() {

    Bundle bundle = new Bundle();
    bundle.putParcelable("instanceState", super.onSaveInstanceState());
    bundle.putString("currentEdit", mEditText.getText().toString());
    bundle.putBoolean("isFocused", mEditText.hasFocus());
    return bundle;

}

@Override
protected void onRestoreInstanceState(Parcelable state) {

    if (state instanceof Bundle) {
        Bundle bundle = (Bundle) state;
        mEditText.setText(bundle.getString("currentEdit"));
        if (bundle.getBoolean("isFocused")) {
            mEditText.requestFocus();
        }
        super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
        return;
    }

    super.onRestoreInstanceState(state);
}
moondroid
  • 1,730
  • 17
  • 20
  • Are you implementing the onSaveInstanceState method in your activity? If so, please post the code. Also please post the code of your activity's onCreate o wherever you are getting your CompoundViews via findViewById. – Aballano Dec 17 '12 at 13:39
  • no, I'm not implementing onSaveInstanceState (see my edit); what seems strange to me is that if I use 2 simple EditText Android is able to correctly manage the state during rotation – moondroid Dec 17 '12 at 14:02
  • 1
    Have a look at this http://stackoverflow.com/questions/3542333/how-to-prevent-custom-views-from-losing-state-across-screen-orientation-changes , Android sees both `EditTexts` with the same id. – user Dec 17 '12 at 14:32
  • Long story short: if you inflate a layout to create a compound view, don't use ids, use tags or `getChildAt()`, and override `onSaveInstanceState()`and `onRestoreInstance()` to handle text keeping through rotation. – Juan José Melero Gómez Mar 15 '17 at 10:49
  • Thanks @moondroid for the solution – Denny Sep 03 '18 at 19:13
  • BTW: Even if you have a TextView together with an EditText, where both are using the same ID, the TextView will also be updated with the value from the EditText. Would be interesting what happens if the second view with the same ID does not even have a setText Method. Would Android still try to call it? – user2808624 Jan 27 '21 at 16:43

9 Answers9

18

You need to disable SaveEnabled property of EditText using android:saveEnabled="false"

In your custom view, you are inflating layout from XML which has ID defined. Android OS has default functionality to save and restore the state of the view if the view has ID defined.

It means that it will save the text of the EditText when Activity gets paused and restore automatically when Activity gets restored. In your case, you are using this custom view multiple times and that is inflating the same layout so your all EditText have the same ID. Now when Activity will get pause Android will retrieve the value of the EditText and will save against their ID but as you have the same ID for each EditText, values will get override and so it will restore same value in all your EditText.

Dharmendra
  • 33,296
  • 22
  • 86
  • 129
10

I'll start off by saying that I haven't confirmed this... but I experienced the same issues when using a compound view, similar to what you were doing.

I think the root of the problem is how Android automatically saves the state of EditText controls, and I think it saves it by "id". So if you have multiple controls in your xml with same "id", then when it saves state, and then restores state, all controls with the same id get the same value. You can try this by adding 2 EditText contols to you normal xml and give them the same android:id value and see if they end up getting the same value.

In your case, you can try to NOT use ids in the compound view and rather find the elements another way, either by tag (View.findViewWithTag), or by name, and see if that makes a difference.

In my case, I solved the problem by doing the latter.

stuckless
  • 6,515
  • 2
  • 19
  • 27
  • 2
    thanks! removing the id has solved the issue; however I have to manage the saving of EditText state in the custom view – moondroid Dec 17 '12 at 15:09
  • Yeah, I forgot to mention that you'll need to manage your own state under this scenario, so that's a good point to know. – stuckless Dec 17 '12 at 16:28
1

I had the same issue, This is how I made it to work. First need to set false for saveEnabled for editText. We can keep android:id in our layout.

<EditText
  android:id="@+id/editText"
  android:saveEnabled="false" 

Then override below methods in your compound view and manage state by your own. Feel free to ask working example if needed.

@Override
protected Parcelable onSaveInstanceState() {
    Bundle bundle = new Bundle();
    bundle.putParcelable("state", super.onSaveInstanceState());
    String text = editText.getText().toString();
    bundle.putString("text", text);
    return bundle;
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    if (state instanceof Bundle) {
        Bundle bundle = (Bundle) state;
        String text = bundle.getString("text");
        state = bundle.getParcelable("state");
        editText.setText(text);
    }
    super.onRestoreInstanceState(state);
}
Krishna Sharma
  • 2,828
  • 1
  • 12
  • 23
0

Take a look at my comment in your question and also make sure that you're getting correctly the references to your views.

I'm using your code like this:

CompoundView cv1 = (CompoundView) findViewById(R.id.compoundview1);
TextView tv1 = (TextView) cv1.findViewById(R.id.textEdit);

CompoundView cv2 = (CompoundView) findViewById(R.id.compoundview2);
TextView tv2 = (TextView) cv2.findViewById(R.id.textEdit);
Aballano
  • 1,037
  • 12
  • 19
  • There is typo: replace cv1 by cv2 at your fourth line: CompoundView cv1 = (CompoundView) findViewById(R.id.compoundview1); TextView tv1 = (TextView) cv1.findViewById(R.id.textEdit); CompoundView cv2 = (CompoundView) findViewById(R.id.compoundview2); TextView tv2 = (TextView) cv1.findViewById(R.id.textEdit); – vsm Dec 17 '12 at 13:50
0

In case you have more complex compound view with many child view, you can consider overriding the dispatchSaveInstanceState of your most outer ViewGroup class and don't call the super implementation.

Like this:

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    //If you don't call super.dispatchRestoreInstanceState(container) here,
    //no child view gets its state saved
}

I use this, because in my case I have hierarchy of many different compound views that have common super class in which I did this override. (I kept forgetting to set the SaveEnabled attribute to false for new layouts.) However there are many caveats, for example you need to manually save focused view and then request its focus, so your app doesn't behave oddly when screen is rotated.

EDIT: If you actually need to save state of your compound view, overriding dispatchSaveInstanceState with an empty body will cause onSave/RestoreInstanceState not being called. If you want to use them and still not save state of any of the child views, you need to call super.dispatchFreezeSelfOnly(container) in your override.

More on this here: http://charlesharley.com/2012/programming/views-saving-instance-state-in-android

Almighty
  • 835
  • 8
  • 16
0

The issue is happening because of the id field on compound_view.xml
From your code, I just noticed that you inflated compound_view layout file in CompoundView class.

As soon as you create compound_view.xml and put android:id="@+id/textLabel" and android:id="@+id/textEdit" id in your layout xml file, android studio automatically create those ids into int values in R class for single time.

So, when you put CompoundView twice time in your activity_main.xml layout file, you just creating two instance of CompoundView but, both instances textEdit and textLabel have only 1 address location for each one. So, they are pointing to same address locations which are declared in R class.


That's why, whenever you change textEdit or textLabel text programatically, they also change other textEdit or textLabel which are presented in both of your CompoundView

Shariful Islam Mubin
  • 2,146
  • 14
  • 23
0

I would like to emphasize a great article, which opened my eyes. It is based on reimplementing onSaveInstanceState() and onRestoreInstanceState(state: Parcelable?).

The advantage of this is that you can use the same compound view multiple times in the same layout (no duplicate ids problem).

Vít Kapitola
  • 499
  • 1
  • 5
  • 9
0

In case someone has troubles with incorrect focus being restored after screen rotation, which occurs due to the shared ids of inner views, you can control which id is saved as focused view by overriding findFocus method like this:

    override fun findFocus(): View {
      if (focusedChild != null) {
          return this
      }

      return super.findFocus()
    }

Then the focus gets restored to your compound view, however you should handle the requestFocus call, so the proper child view gets focus upon restoration.

Almighty
  • 835
  • 8
  • 16
-1

I got the same annoying problem and solved it with another solution than the ones suggested. When the custom view is inflated, I don't find them with IDs or tags, I get them using the getChildAt method. I did not have to save the instance state.

Here is an example:

Custom XML to inflate (custom_view.xml):

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" >
    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"
        android:textAppearance="?android:attr/textAppearanceMedium"/>
    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:gravity="center"/>
</merge>

You then just get the views this way during the view initialization:

private void initView(Context context) {
    LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    inflater.inflate(R.layout.custom_view, this, true);

    setOrientation(LinearLayout.HORIZONTAL);
    setGravity(Gravity.CENTER);

    TextView mTitleTV = (TextView) getChildAt(0);
    EditText mContentET = (EditText) getChildAt(1);
}

IMPORTANT: you must remove all IDs from the XML

Yoann Hercouet
  • 17,894
  • 5
  • 58
  • 85
  • 2
    This will be a maintenance nightmare because any change of the order in the XML layout will have to be reflected in the indices passed to getChildAt(). – Mark Herscher Nov 16 '16 at 19:04