Reputation: 116
I am customizing an Android control. This control uses a merge layout containing an edit field (fluff left out):
<merge xmlns:android="http://schemas.android.com/apk/res/android">
...
<EditText android:id="@+id/numberpicker_input">
...
/>
...
</merge>
This works fine for a single control. However, if I put multiple instances of this control in a layout, it results in weird behaviour when rotating a device from portrait to landscape. All controls get the same value restored and callbacks get attached to the wrong control instance.
Upon examination, it emerged that the embedded edit controls have the exact same id value.
The relevant java source looks like this:
public class NumberPicker extends LinearLayout implements OnClickListener,
OnFocusChangeListener, OnLongClickListener, OnEditorActionListener {
private final EditText mText;
....
public NumberPicker(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
Log.d(TAG, "Numberpicker create, have id: " + getId() );
setOrientation(VERTICAL);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.number_picker, this, true);
...
mText = (EditText) findViewById(R.id.numberpicker_input);
...
}
}
Now, consider the value of R.id.numberpicker_input, when you have three separate instances of a NumberPicker in a layout. In each instance, the edittext field will have the same id.
To get the custom control to work properly, I need the id's of the embedded edit controls to be unique. How can I fix this?
Upvotes: 3
Views: 772
Reputation: 1
Another possibility to get new resource ids is this little hack.
public class MyBaseDialogFragment extends DialogFragment implements
ResourceIdGetter {
Random mRandom;
@Override
public int getNewResourceId() {
if(mRandom == null)
mRandom = new Random(123456789); // make them predictable
int i;
while (true) {
i = mRandom.nextInt();
if(i <= 0)
continue;
try {
getResources().getResourceName(i);
} catch (NotFoundException e) {
break;
}
}
return i;
}
}
After that, you can reassign new unique resource id's to controls with ids:
protected mNewResorceId;
public void addEditViewToLinearLayout(LinearLayout pContainer,
LayoutInflater pInflater, ResourceIdGetter pRes) {
View a_multiple_inflated_layout = pInflater.inflate(
R.layout.a_multiple_inflated_layout, pContainer, false);
mNewResorceId = pRes.getNewResourceId()
a_multiple_inflated_layout.findViewById(R.id.child_view_with_id).setId(
mNewResorceId);
pContainer.addView(a_multiple_inflated_layout, new LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
}
After that, you have to use findViewById(mNewResorceId) to get the view. Because of the given seed for Random, the assigned numbers are always the same. That means, even if the activity is destroyed and recreated, things like cursor position are restored.
Upvotes: 0
Reputation: 116
Here's another option, with onSave/RestoreInstanceState()
. Doing this the usual way doesn't work, because the framework will restore the EditText value right after the call to onRestoreInstanceState()
, overwriting anything you had restored.
To avoid this, clear the id of the embedded control in the constructor of the compound control, eg.:
mText.setId( NO_ID );
Layout elements with id set are ignored while saving/restoring.
Now, save and restore the embedded control value in the compound control state:
protected Parcelable onSaveInstanceState() {
Parcelable p = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putString( "EDIT_TEXT", mText.getText().toString() );
bundle.putParcelable("SUPER", p);
return bundle;
}
protected void onRestoreInstanceState(Parcelable parcel) {
Bundle bundle = (Bundle) parcel;
mText.setText( bundle.getString("EDIT_TEXT") );
super.onRestoreInstanceState(bundle.getParcelable("SUPER"));
}
The restored state is specific to a given instance of the compound object, therefore there will be no interference between multiple instances in the same view.
The advantage of this wrt. previous answer is that no fiddling with id's is necessary.
Upvotes: 4
Reputation: 116
I've been looking at the code for saving instance state in the Android libs. What I noticed is that the control/layout id's are used as keys in the saved state. I think here lies the problem: the id's of the embedded elements in the multiple instances of the compound objects are the same. So on save, the state of just one embedded element is saved, the rest is discarded.
I think the solution lies in making the id's of the embedded elements unique. My solution is this:
Define an attribute which passes an id for the embedded control:
<resources>
<declare-styleable name="compoundcontrol">
<attr name="idEmbedded" format="reference" />
</declare-styleable>
</resources>
Pass this id as parameter to the compound control in the layout:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:np="http://schemas.android.com/apk/res/org.example.compoundcontrol"
...
/>
...
<org.example.compoundcontrol.CompoundControl
android:id = "@+id/compound_id"
np:idEmbedded="@+id/embedded_id"
/>
...
In the constructor of the compound object, read in this attribute value and use it to set the id of the embedded control.
public CompoundControl(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.compoundcontrol );
int new_id = a.getResourceId( R.styleable.compoundcontrol_idEmbedded, DEFAULT_VALUE );
a.recycle();
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.compoundcontrol, this, true);
View mEmbedded = findViewById(R.id.embedded_control);
mEmbedded.setId( new_id );
}
It is unfortunate that the id of the embedded control has to be set so explicitly, but it does the trick.
Upvotes: 0
Reputation: 68187
you may create unique IDs in xml resources as below:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="firstButton"/>
<item type="id" name="secondButton"/>
<item type="id" name="thirdButton"/>
</resources>
and in code, design your EditTexts programmatically and set their ids like:
txt1.setId(R.id.firstButton);
txt2.setId(R.id.secondButton);
txt3.setId(R.id.thirdButton);
for more info, read this
Update.....
another approach would be to iterate through all the views and set them unique ids yourself and keep track of those assigned ids:
first of all, set an id to your parent layout in XML file. It will help us to retrieve the entire layout as ViewGroup. In this example I've set my LinearLayout the id parentLayout
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.test_activity);
ViewGroup parent = (ViewGroup)findViewById(R.id.parentLayout);
for(int i=0; i<parent.getChildCount(); i++){
//at this point i dont know the Id of edittext yet
View v = parent.getChildAt(i);
//if the view is an instance of EditText then lets set to it a new Id
if(v instanceof EditText){
EditText et = (EditText) v;
et.setId(123456);
Toast.makeText(this,"EditText's id: " + et.getId(),
Toast.LENGTH_SHORT).show();
}
}
}
I guess using this technique, you may even assign some random ids to all your views in order to avoid any id collision.
Upvotes: 1