Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android Compound Custom View restore save state

I created compound custom view which contains TextView and EditText called LabledEditText, since I will have a lot of EditText fields in a fragment.

I have created an XML file that holds the following

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/linearLayoutLabeledEditText"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/label_textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/label"
        android:textAppearance="?android:attr/textAppearanceMedium" />

    <EditText
        android:id="@+id/value_editText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:freezesText="true"
        android:saveEnabled="true"/>
</LinearLayout>

and in the View class is as following

public class LabeledEditText extends LinearLayout {
    private EditText editText;
    private TextView label;

    public LabeledEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        inflate(context,R.layout.labeled_edit_text, this);
        label = (TextView) this.findViewById(R.id.label_textView);
        label.setText("some label");
        editText = (EditText) this.findViewById(R.id.value_editText);
    }

    protected void onRestoreInstanceState(Parcelable state) {
        String id= this.getId()+" ";
        if (state instanceof Bundle) // implicit null check
        {
            Bundle bundle = (Bundle) state;
            state = bundle.getParcelable(id+"super");
            super.onRestoreInstanceState(state);
            editText.setText(bundle.getString(id+"editText"));
        }
    }

    protected Parcelable onSaveInstanceState() {
        String id= this.getId()+" ";
        Bundle bundle = new Bundle();
        bundle.putParcelable(id+"super",super.onSaveInstanceState());
        bundle.putString(id+"editText",editText.getText().toString());
        return bundle;
    }   
}

then I use it in a 3 fragments that represent 3 steps. When I insert values in the first 1 step/fragment

1st time after visiting the first step

then switch to other fragments and return to the 1 step/fragment again I find the following

2nd time visiting the first step

what is causing this problem ?

I have been debugging it for at least 5 days, keeping in mind that each of those custom views has different id when used inside the fragment layout.

I also have tried to add the id of the custom view as part of the key during saving the state this.getId()+"editText" still the same problem.

EDIT the genrateViewId for api < 17

the code after alteration

import java.util.concurrent.atomic.AtomicInteger;

public class LabeledEditText extends LinearLayout {
    private EditText editText;
    private TextView label;

    public LabeledEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        inflate(context,R.layout.labeled_edit_text, this);
        label = (TextView) this.findViewById(R.id.label_textView);
        editText = (EditText) this.findViewById(R.id.value_editText);
        editText.setId(generateViewId());
        applyAttr(context,attrs);
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        //adding the id of the parent view as part of the key so that
        //editText state won't get overwritten by other editText 
        //holding the same id
        bundle.putParcelable("super",super.onSaveInstanceState());
        bundle.putString("editText",editText.getText().toString());
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (state instanceof Bundle) // implicit null check
        {
            Bundle bundle = (Bundle) state;
            state = bundle.getParcelable("super");
            super.onRestoreInstanceState(state);
            editText.setText(bundle.getString("editText"));
        }
    }

    private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);

    public static int generateViewId() {
        for (;;) {
            final int result = sNextGeneratedId.get();
            // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
            int newValue = result + 1;
            if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
            if (sNextGeneratedId.compareAndSet(result, newValue)) {
                return result;
            }
        }
    }
}
like image 818
john-salib Avatar asked Aug 28 '16 12:08

john-salib


1 Answers

Your problem is that you have multiple ViewGroups (LinearLayout), with children having same ids. Therefore when saving state, all of them are being saves in same state, and the last one, overwrites all.
To solve this, you have to give each view a unique I'd when you inflate. In v17 and later you can use View.generateViewId();, in older versions you will have to create static ids manually in the ids file.
Your code should look like this;

public LabeledEditText(Context context, AttributeSet attrs) { 
    super(context, attrs); inflate(context,R.layout.labeled_edit_text, this); 
    label = (TextView) this.findViewById(R.id.label_textView); label.setText("some label"); 
    editText = (EditText) this.findViewById(R.id.value_editText); 
    label.setId(View.generateViewId());
    editText.setId(View.generateViewId());
}

In any case it may be better to use static ids, as it would be easier to reference them later. You may not even need anymore to overwrite the onSave and onRestore, especially if you use static ids.

like image 125
lionscribe Avatar answered Nov 06 '22 22:11

lionscribe