我的自定义视图不会保存它的状态
My custom view will not save it's state
我使用以下代码创建了自定义视图(为简洁起见进行了编辑):
public class StampView extends View {
// View State items
private String stampText, stampTimeStamp;
private int stampIconId;
private boolean stamped;
public StampView(Context context, @Nullable AttributeSet attrs) {
// REMOVED
}
/**
*
* See: http://trickyandroid.com/saving-android-view-state-correctly/
*
* @return Returns a Parcelable object containing the view's current dynamic
* state, or null if there is nothing interesting to save.
* @see #onRestoreInstanceState(Parcelable)
* @see #saveHierarchyState(SparseArray)
* @see #dispatchSaveInstanceState(SparseArray)
* @see #setSaveEnabled(boolean)
*/
@Nullable @Override protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
// We need to save 4 extra things to save state.
// StampText string
ss.stampText = stampText;
// Icon ID int
ss.stampIconId = stampIconId;
// Timestamp string
ss.stampTimestamp = stampTimeStamp;
// Stamped boolean
ss.stamped = stamped;
// An example
// ss.state = customState;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
// our custom saved state items
setStampIcon(ss.stampIconId);
setStampTitleText(ss.stampText);
setStampTimeStampText(ss.stampTimestamp);
setStamped(ss.stamped);
}
/**
* Extending the BaseSavedState allows us to extend state saving and save our own custom state variables above.
*
**/
static class SavedState extends BaseSavedState {
int stampIconId;
String stampText, stampTimestamp;
boolean stamped;
SavedState(Parcelable superState) {
super(superState);
}
// Reading In from a Parcel
private SavedState(Parcel in) {
super(in);
stampIconId = in.readInt();
stampText = in.readString();
stampTimestamp = in.readString();
stamped = in.readByte() != 0; //myBoolean == true if byte != 0
}
// Writing to a Parcel
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
// I assume order matters, since we don't seem to be writing the key values
out.writeInt(stampIconId);
out.writeString(stampText);
out.writeString(stampTimestamp);
out.writeByte((byte) (stamped ? 1 : 0)); //if myBoolean == true, byte == 1
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//REMOVED
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Rect bounds = new Rect(0,0,mWidth,mHeight);
if(stamped){
// Draw the stamp
} else {
// Draw a placeholder instead
}
}
public void constructStampViewFromPOI(POI poi){
//removed
}
private void setStampTitleText (String titleString){
this.stampText = titleString;
}
private void setStampTimeStampText (String tsString){
this.stampTimeStamp = tsString;
}
private void setStampIcon(int collectionIconId) {
// removed
}
public void setStamped(boolean bool) {
this.stamped = bool;
this.invalidate();
}
}
我的自定义视图相当简单,它可以绘制出自己的 2 个版本,这取决于一个名为 'stamped' 的布尔值。您可以在 draw 方法中看到这一点。我将这个自定义视图放在一个包含 6 个项目的 RecyclerView 中的 Viewholder 中。我的 activity 应具有以下布局:
Viewholder #1 - Stamp view (#1 data)
Viewholder #2 - Placeholder view
Viewholder #3 - Placeholder view
Viewholder #4 - Placeholder view
Viewholder #5 - Placeholder view
Viewholder #6 - Stamped view (#6 data)
但是如果我调试我的 activity 我看到我的自定义视图被绘制了 6 次,一次是为 recyclerview 中的每个 viewholder 绘制一次,然后由于某种原因它再次重新绘制所有 6 次。在第二遍中,它使用 Viewholder #6 的数据弄乱了 Viewholder #5 中 StampView 的状态。我最终得到了这个布局:
Viewholder #1 - Stamp view (#1 data)
Viewholder #2 - Placeholder view
Viewholder #3 - Placeholder view
Viewholder #4 - Placeholder view
Viewholder #5 - Stamped view (#6 data)
Viewholder #6 - Stamped view (#6 data)
点击查看器会将我带到 DetailsActivity,如果我这样做然后 return 回到我的 RecyclerView 我看到这个布局:
Viewholder #1 - Stamp view (#1 data)
Viewholder #2 - Placeholder view
Viewholder #3 - Placeholder view
Viewholder #4 - Stamped view (#6 data)
Viewholder #5 - Stamped view (#6 data)
Viewholder #6 - Stamped view (#6 data)
如果我再这样做两次,我最终会得到这个乱七八糟的布局:
Viewholder #1 - Stamp view (#1 data)
Viewholder #2 - Stamped view (#6 data)
Viewholder #3 - Stamped view (#6 data)
Viewholder #4 - Stamped view (#6 data)
Viewholder #5 - Stamped view (#6 data)
Viewholder #6 - Stamped view (#6 data)
在我看来,我的自定义视图没有保存它的状态 and/or 当我在屏幕上有多个实例时会混淆它的状态。我发现 this post 提到了这类问题,并试图实现它保存状态的方式,但我无法在我的视图中触发 onSaveInstanceState 方法。我已经尝试了提到的所有内容,包括 setSaveEnabled(true);
在我的自定义视图和 viewholder 中,我已经尝试为自定义视图的每个实例设置一个自定义 id...但没有任何效果。
- 有人可以向我解释我做错了什么吗?
- 我是否正确认为这是一个状态保存问题?
- 我应该如何以及在何处保存我的自定义视图状态?在视图内部还是 activity 应该保存我的视图状态,因为它是视图持有者的一部分?
编辑:
如果有帮助,这里是查看器代码:
@Override
public void onBindViewHolder(POI_View_Holder holder, int position) {
//Instead lets use the viewholder bind method to assign content
holder.bind(mPOIslist.get(position), listener);
}
static class POI_View_Holder extends RecyclerView.ViewHolder {
// Variables for the ViewHolder
private TextView name;
private TextView positionText;
private StampView stampView;
POI_View_Holder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.poi_nameTV);
positionText= itemView.findViewById(R.id.poi_positionTV);
stampView = itemView.findViewById(R.id.poi_StampView);
// stampView.setSaveEnabled(true); // force state saving
// stampView.setId(getPosition());
}
public void bind(final POI poi, final OnPOIClickListener listener){
// Set POI information in viewHolder
name.setText(poi.getName());
positionText.setText(String.valueOf(poi.getCollectionPosition()));
if(poi.isStamped()){
stampView.setElevation(4);
stampView.setTranslationZ(4);
stampView.setClipToOutline(true);
// stampView.invalidate();
stampView.constructStampViewFromPOI(poi);
} else {
stampView.setElevation(0);
stampView.setTranslationZ(0);
}
Log.d("ViewHolder-", "bind: method fired");
itemView.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
listener.OnPOIClick(poi);
}
});
}
}
EDIT2:这是我的 activity 保存状态代码:
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
// Save layoutManager state
mListState = mLayoutManager.onSaveInstanceState();
state.putParcelable(LIST_STATE_KEY, mListState);
mCollectionList = Parcels.wrap(mListOfPOIsInCollection);
state.putParcelable(COLLECTION_ARRAYLIST_STATE_KEY,mCollectionList);
}
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
// Retrieve list state and list/item positions
if(state != null) {
mListState = state.getParcelable(LIST_STATE_KEY);
//mCollectionList = state.getParcelable(COLLECTION_ARRAYLIST_STATE_KEY);
mListOfPOIsInCollection = Parcels.unwrap(state.getParcelable(COLLECTION_ARRAYLIST_STATE_KEY));
}
}
@Override protected void onResume() {
super.onResume();
if (mListState != null) {
mLayoutManager.onRestoreInstanceState(mListState);
}
}
View.onSaveInstanceState()
和 View.onRestoreInstanceState(Parcelable)
意味着当您的视图被销毁然后与 activity 一起重新创建时由系统调用,也许当您旋转 phone. RecyclerView
.
没有以任何方式使用它们
我觉得你至少有一个问题,可能有两个。
首先,在适配器的 onBindViewHolder()
方法中,您需要确保始终设置每个 sub-view 的状态。人们 运行 经常遇到这样的问题,他们有时只设置 sub-view 的状态,这会在回收视图时导致问题。例如:
if (myItem.isStamped()) {
holder.stampView.setStamped(true);
}
如果您的视图被回收并 re-bound 到 non-stamped 数据项,这将中断。因为您只在印记存在时设置 holder.stampView
的状态,所以当印记不存在时您不会 清除 它。相反,你应该写:
if (myItem.isStamped()) {
holder.stampView.setStamped(true);
} else {
holder.stampView.setStamped(false);
}
或者只是
holder.stampView.setStamped(myItem.isStamped());
其次,您的适配器数据听起来可能 saved/restored 不正确。我真的不能说这个,因为我没有你的任何 activity 代码,但你可以利用 Activity.onSaveInstanceState(Bundle)
和传递给 onCreate()
的 savedInstanceState
包来保存它。
我使用以下代码创建了自定义视图(为简洁起见进行了编辑):
public class StampView extends View {
// View State items
private String stampText, stampTimeStamp;
private int stampIconId;
private boolean stamped;
public StampView(Context context, @Nullable AttributeSet attrs) {
// REMOVED
}
/**
*
* See: http://trickyandroid.com/saving-android-view-state-correctly/
*
* @return Returns a Parcelable object containing the view's current dynamic
* state, or null if there is nothing interesting to save.
* @see #onRestoreInstanceState(Parcelable)
* @see #saveHierarchyState(SparseArray)
* @see #dispatchSaveInstanceState(SparseArray)
* @see #setSaveEnabled(boolean)
*/
@Nullable @Override protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
// We need to save 4 extra things to save state.
// StampText string
ss.stampText = stampText;
// Icon ID int
ss.stampIconId = stampIconId;
// Timestamp string
ss.stampTimestamp = stampTimeStamp;
// Stamped boolean
ss.stamped = stamped;
// An example
// ss.state = customState;
return ss;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
// our custom saved state items
setStampIcon(ss.stampIconId);
setStampTitleText(ss.stampText);
setStampTimeStampText(ss.stampTimestamp);
setStamped(ss.stamped);
}
/**
* Extending the BaseSavedState allows us to extend state saving and save our own custom state variables above.
*
**/
static class SavedState extends BaseSavedState {
int stampIconId;
String stampText, stampTimestamp;
boolean stamped;
SavedState(Parcelable superState) {
super(superState);
}
// Reading In from a Parcel
private SavedState(Parcel in) {
super(in);
stampIconId = in.readInt();
stampText = in.readString();
stampTimestamp = in.readString();
stamped = in.readByte() != 0; //myBoolean == true if byte != 0
}
// Writing to a Parcel
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
// I assume order matters, since we don't seem to be writing the key values
out.writeInt(stampIconId);
out.writeString(stampText);
out.writeString(stampTimestamp);
out.writeByte((byte) (stamped ? 1 : 0)); //if myBoolean == true, byte == 1
}
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//REMOVED
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Rect bounds = new Rect(0,0,mWidth,mHeight);
if(stamped){
// Draw the stamp
} else {
// Draw a placeholder instead
}
}
public void constructStampViewFromPOI(POI poi){
//removed
}
private void setStampTitleText (String titleString){
this.stampText = titleString;
}
private void setStampTimeStampText (String tsString){
this.stampTimeStamp = tsString;
}
private void setStampIcon(int collectionIconId) {
// removed
}
public void setStamped(boolean bool) {
this.stamped = bool;
this.invalidate();
}
}
我的自定义视图相当简单,它可以绘制出自己的 2 个版本,这取决于一个名为 'stamped' 的布尔值。您可以在 draw 方法中看到这一点。我将这个自定义视图放在一个包含 6 个项目的 RecyclerView 中的 Viewholder 中。我的 activity 应具有以下布局:
Viewholder #1 - Stamp view (#1 data)
Viewholder #2 - Placeholder view
Viewholder #3 - Placeholder view
Viewholder #4 - Placeholder view
Viewholder #5 - Placeholder view
Viewholder #6 - Stamped view (#6 data)
但是如果我调试我的 activity 我看到我的自定义视图被绘制了 6 次,一次是为 recyclerview 中的每个 viewholder 绘制一次,然后由于某种原因它再次重新绘制所有 6 次。在第二遍中,它使用 Viewholder #6 的数据弄乱了 Viewholder #5 中 StampView 的状态。我最终得到了这个布局:
Viewholder #1 - Stamp view (#1 data)
Viewholder #2 - Placeholder view
Viewholder #3 - Placeholder view
Viewholder #4 - Placeholder view
Viewholder #5 - Stamped view (#6 data)
Viewholder #6 - Stamped view (#6 data)
点击查看器会将我带到 DetailsActivity,如果我这样做然后 return 回到我的 RecyclerView 我看到这个布局:
Viewholder #1 - Stamp view (#1 data)
Viewholder #2 - Placeholder view
Viewholder #3 - Placeholder view
Viewholder #4 - Stamped view (#6 data)
Viewholder #5 - Stamped view (#6 data)
Viewholder #6 - Stamped view (#6 data)
如果我再这样做两次,我最终会得到这个乱七八糟的布局:
Viewholder #1 - Stamp view (#1 data)
Viewholder #2 - Stamped view (#6 data)
Viewholder #3 - Stamped view (#6 data)
Viewholder #4 - Stamped view (#6 data)
Viewholder #5 - Stamped view (#6 data)
Viewholder #6 - Stamped view (#6 data)
在我看来,我的自定义视图没有保存它的状态 and/or 当我在屏幕上有多个实例时会混淆它的状态。我发现 this post 提到了这类问题,并试图实现它保存状态的方式,但我无法在我的视图中触发 onSaveInstanceState 方法。我已经尝试了提到的所有内容,包括 setSaveEnabled(true);
在我的自定义视图和 viewholder 中,我已经尝试为自定义视图的每个实例设置一个自定义 id...但没有任何效果。
- 有人可以向我解释我做错了什么吗?
- 我是否正确认为这是一个状态保存问题?
- 我应该如何以及在何处保存我的自定义视图状态?在视图内部还是 activity 应该保存我的视图状态,因为它是视图持有者的一部分?
编辑: 如果有帮助,这里是查看器代码:
@Override
public void onBindViewHolder(POI_View_Holder holder, int position) {
//Instead lets use the viewholder bind method to assign content
holder.bind(mPOIslist.get(position), listener);
}
static class POI_View_Holder extends RecyclerView.ViewHolder {
// Variables for the ViewHolder
private TextView name;
private TextView positionText;
private StampView stampView;
POI_View_Holder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.poi_nameTV);
positionText= itemView.findViewById(R.id.poi_positionTV);
stampView = itemView.findViewById(R.id.poi_StampView);
// stampView.setSaveEnabled(true); // force state saving
// stampView.setId(getPosition());
}
public void bind(final POI poi, final OnPOIClickListener listener){
// Set POI information in viewHolder
name.setText(poi.getName());
positionText.setText(String.valueOf(poi.getCollectionPosition()));
if(poi.isStamped()){
stampView.setElevation(4);
stampView.setTranslationZ(4);
stampView.setClipToOutline(true);
// stampView.invalidate();
stampView.constructStampViewFromPOI(poi);
} else {
stampView.setElevation(0);
stampView.setTranslationZ(0);
}
Log.d("ViewHolder-", "bind: method fired");
itemView.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
listener.OnPOIClick(poi);
}
});
}
}
EDIT2:这是我的 activity 保存状态代码:
protected void onSaveInstanceState(Bundle state) {
super.onSaveInstanceState(state);
// Save layoutManager state
mListState = mLayoutManager.onSaveInstanceState();
state.putParcelable(LIST_STATE_KEY, mListState);
mCollectionList = Parcels.wrap(mListOfPOIsInCollection);
state.putParcelable(COLLECTION_ARRAYLIST_STATE_KEY,mCollectionList);
}
protected void onRestoreInstanceState(Bundle state) {
super.onRestoreInstanceState(state);
// Retrieve list state and list/item positions
if(state != null) {
mListState = state.getParcelable(LIST_STATE_KEY);
//mCollectionList = state.getParcelable(COLLECTION_ARRAYLIST_STATE_KEY);
mListOfPOIsInCollection = Parcels.unwrap(state.getParcelable(COLLECTION_ARRAYLIST_STATE_KEY));
}
}
@Override protected void onResume() {
super.onResume();
if (mListState != null) {
mLayoutManager.onRestoreInstanceState(mListState);
}
}
View.onSaveInstanceState()
和 View.onRestoreInstanceState(Parcelable)
意味着当您的视图被销毁然后与 activity 一起重新创建时由系统调用,也许当您旋转 phone. RecyclerView
.
我觉得你至少有一个问题,可能有两个。
首先,在适配器的 onBindViewHolder()
方法中,您需要确保始终设置每个 sub-view 的状态。人们 运行 经常遇到这样的问题,他们有时只设置 sub-view 的状态,这会在回收视图时导致问题。例如:
if (myItem.isStamped()) {
holder.stampView.setStamped(true);
}
如果您的视图被回收并 re-bound 到 non-stamped 数据项,这将中断。因为您只在印记存在时设置 holder.stampView
的状态,所以当印记不存在时您不会 清除 它。相反,你应该写:
if (myItem.isStamped()) {
holder.stampView.setStamped(true);
} else {
holder.stampView.setStamped(false);
}
或者只是
holder.stampView.setStamped(myItem.isStamped());
其次,您的适配器数据听起来可能 saved/restored 不正确。我真的不能说这个,因为我没有你的任何 activity 代码,但你可以利用 Activity.onSaveInstanceState(Bundle)
和传递给 onCreate()
的 savedInstanceState
包来保存它。