วิธีป้องกันมุมมองที่กำหนดเองจากการสูญเสียสถานะการเปลี่ยนแปลงการวางแนวหน้าจอ


248

ฉันได้ติดตั้งสำเร็จแล้วonRetainNonConfigurationInstance()สำหรับอุปกรณ์หลักของฉันActivityในการบันทึกและกู้คืนส่วนประกอบที่สำคัญบางอย่างในการเปลี่ยนแปลงการวางแนวหน้าจอ

แต่ดูเหมือนว่ามุมมองที่กำหนดเองของฉันจะถูกสร้างขึ้นใหม่ตั้งแต่ต้นเมื่อการวางแนวเปลี่ยนไป ถึงแม้ว่าในกรณีของฉันมันจะไม่สะดวกเพราะมุมมองที่กำหนดเองในคำถามคือพล็อต X / Y และจุดพล็อตจะถูกเก็บไว้ในมุมมองที่กำหนดเอง

มีวิธีที่มีฝีมือในการใช้บางสิ่งที่คล้ายกับonRetainNonConfigurationInstance()สำหรับมุมมองที่กำหนดเองหรือฉันต้องใช้วิธีการในมุมมองที่กำหนดเองซึ่งอนุญาตให้ฉันรับและตั้งค่า "สถานะ" ของมันได้หรือไม่

คำตอบ:


415

คุณทำได้โดยการดำเนินการView#onSaveInstanceStateและView#onRestoreInstanceStateและการขยายView.BaseSavedStateชั้นเรียน

public class CustomView extends View {

  private int stateToSave;

  ...

  @Override
  public Parcelable onSaveInstanceState() {
    //begin boilerplate code that allows parent classes to save state
    Parcelable superState = super.onSaveInstanceState();

    SavedState ss = new SavedState(superState);
    //end

    ss.stateToSave = this.stateToSave;

    return ss;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state) {
    //begin boilerplate code so parent classes can restore state
    if(!(state instanceof SavedState)) {
      super.onRestoreInstanceState(state);
      return;
    }

    SavedState ss = (SavedState)state;
    super.onRestoreInstanceState(ss.getSuperState());
    //end

    this.stateToSave = ss.stateToSave;
  }

  static class SavedState extends BaseSavedState {
    int stateToSave;

    SavedState(Parcelable superState) {
      super(superState);
    }

    private SavedState(Parcel in) {
      super(in);
      this.stateToSave = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel out, int flags) {
      super.writeToParcel(out, flags);
      out.writeInt(this.stateToSave);
    }

    //required field that makes Parcelables from a Parcel
    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];
          }
    };
  }
}

งานถูกแบ่งระหว่างคลาส View และ SavedState ของคลาส คุณควรทำงานอ่านและเขียนทั้งหมดจากและParcelในSavedStateชั้นเรียน จากนั้นคลาสมุมมองของคุณสามารถทำงานเพื่อแยกสมาชิกรัฐและทำงานที่จำเป็นเพื่อให้คลาสกลับสู่สถานะที่ถูกต้อง

หมายเหตุ: View#onSavedInstanceStateและView#onRestoreInstanceStateจะถูกเรียกโดยอัตโนมัติสำหรับคุณหากView#getIdส่งคืนค่า> = 0 สิ่งนี้จะเกิดขึ้นเมื่อคุณให้รหัสใน xml หรือโทรsetIdด้วยตนเอง มิฉะนั้นคุณมีการโทรView#onSaveInstanceStateและเขียน Parcelable กลับไปพัสดุที่คุณได้รับในการActivity#onSaveInstanceStateบันทึกสถานะและต่อมาอ่านมันและผ่านมันไปจากView#onRestoreInstanceStateActivity#onRestoreInstanceState

อีกตัวอย่างที่ง่ายของเรื่องนี้คือ CompoundButton


14
สำหรับผู้ที่มาถึงที่นี่เพราะสิ่งนี้ไม่ทำงานเมื่อใช้ Fragments กับไลบรารีการสนับสนุน v4 ฉันทราบว่าไลบรารีการสนับสนุนดูเหมือนจะไม่เรียก ViewS onSaveInstanceState / onRestoreInstanceState ของ View คุณต้องเรียกตัวเองอย่างชัดเจนจากที่ที่สะดวกใน FragmentActivity หรือ Fragment
MagneticMonster

69
โปรดทราบว่า CustomView ที่คุณใช้นี้ควรมีชุดรหัสที่ไม่ซ้ำกันมิฉะนั้นจะใช้สถานะร่วมกัน SavedState จะถูกเก็บไว้กับ ID ของ CustomView ดังนั้นหากคุณมี CustomViews หลายตัวที่มี id เดียวกันหรือไม่มี id ดังนั้นพัสดุที่บันทึกไว้ใน CustomView สุดท้ายจะถูกส่งผ่านไปยัง CustomView.onRestoreInstanceState () ทั้งหมด เรียกคืนมุมมอง
Nick Street

5
วิธีนี้ใช้ไม่ได้กับฉันด้วยมุมมองที่กำหนดเองสองมุมมอง ฉันยังคงได้รับ ClassNotFoundException เมื่อคืนมุมมองของฉัน ฉันต้องใช้วิธี Bundle ในคำตอบของ Kobor42
Chris Feist

3
onSaveInstanceState()และonRestoreInstanceState()ควรจะเป็นprotected(เช่น superclass ของพวกเขา) publicไม่ได้ ไม่มีเหตุผลที่จะเปิดเผยพวกเขา ...
XåpplI'-I0llwlg'I -

7
สิ่งนี้ใช้งานไม่ได้เมื่อบันทึกแบบกำหนดเองBaseSaveStateสำหรับคลาสที่ขยาย RecyclerView คุณจะได้รับParcel﹕ Class not found when unmarshalling: android.support.v7.widget.RecyclerView$SavedState java.lang.ClassNotFoundException: android.support.v7.widget.RecyclerView$SavedStateดังนั้นคุณต้องแก้ไขข้อบกพร่องที่เขียนไว้ที่นี่: github.com/ksoichiro/Android-ObservableScrollView/commit/ ...... (ใช้ ClassLoader ของ RecyclerView.class เพื่อโหลดสถานะซุปเปอร์)
EpicPandaForce

459

ฉันคิดว่านี่เป็นเวอร์ชั่นที่ง่ายกว่ามาก Bundleเป็นชนิดในตัวซึ่งใช้Parcelable

public class CustomView extends View
{
  private int stuff; // stuff

  @Override
  public Parcelable onSaveInstanceState()
  {
    Bundle bundle = new Bundle();
    bundle.putParcelable("superState", super.onSaveInstanceState());
    bundle.putInt("stuff", this.stuff); // ... save stuff 
    return bundle;
  }

  @Override
  public void onRestoreInstanceState(Parcelable state)
  {
    if (state instanceof Bundle) // implicit null check
    {
      Bundle bundle = (Bundle) state;
      this.stuff = bundle.getInt("stuff"); // ... load stuff
      state = bundle.getParcelable("superState");
    }
    super.onRestoreInstanceState(state);
  }
}

5
ทำไมจะไม่ onRestoreInstanceStateถูกเรียกใช้ Bundle หากonSaveInstanceStateส่งคืน Bundle
Qwertie

5
OnRestoreInstanceถูกสืบทอด เราไม่สามารถเปลี่ยนส่วนหัว Parcelableเป็นเพียงส่วนต่อประสานและBundleนำไปปฏิบัติ
Kobor42

5
ขอบคุณด้วยวิธีนี้จะดีกว่ามากและหลีกเลี่ยง BadParcelableException เมื่อใช้เฟรมเวิร์ก SavedState สำหรับมุมมองที่กำหนดเองเนื่องจากสถานะที่บันทึกไว้ดูเหมือนจะไม่สามารถตั้งค่าตัวโหลดคลาสอย่างถูกต้องสำหรับ SavedState ที่กำหนดเองของคุณ!
Ian Warwick

3
ฉันมีหลายอินสแตนซ์ของมุมมองเดียวกันในกิจกรรม พวกเขาทั้งหมดมีรหัสเฉพาะใน xml แต่พวกเขาทั้งหมดยังได้รับการตั้งค่าของมุมมองล่าสุด ความคิดใด ๆ
Christoffer

15
วิธีนี้อาจใช้ได้ แต่ไม่ปลอดภัยแน่นอน โดยการดำเนินการนี้คุณกำลังสมมติว่าฐานรัฐไม่ได้เป็นView Bundleแน่นอนว่าเป็นจริงในขณะนี้ แต่คุณต้องพึ่งพาความจริงในการใช้งานในปัจจุบันซึ่งไม่รับประกันว่าจะเป็นจริง
Dmitry Zaytsev

18

นี่คืออีกหนึ่งตัวแปรที่ใช้การผสมผสานของทั้งสองวิธีข้างต้น รวมความเร็วและความถูกต้องของParcelableกับความเรียบง่ายของBundle:

@Override
public Parcelable onSaveInstanceState() {
    Bundle bundle = new Bundle();
    // The vars you want to save - in this instance a string and a boolean
    String someString = "something";
    boolean someBoolean = true;
    State state = new State(super.onSaveInstanceState(), someString, someBoolean);
    bundle.putParcelable(State.STATE, state);
    return bundle;
}

@Override
public void onRestoreInstanceState(Parcelable state) {
    if (state instanceof Bundle) {
        Bundle bundle = (Bundle) state;
        State customViewState = (State) bundle.getParcelable(State.STATE);
        // The vars you saved - do whatever you want with them
        String someString = customViewState.getText();
        boolean someBoolean = customViewState.isSomethingShowing());
        super.onRestoreInstanceState(customViewState.getSuperState());
        return;
    }
    // Stops a bug with the wrong state being passed to the super
    super.onRestoreInstanceState(BaseSavedState.EMPTY_STATE); 
}

protected static class State extends BaseSavedState {
    protected static final String STATE = "YourCustomView.STATE";

    private final String someText;
    private final boolean somethingShowing;

    public State(Parcelable superState, String someText, boolean somethingShowing) {
        super(superState);
        this.someText = someText;
        this.somethingShowing = somethingShowing;
    }

    public String getText(){
        return this.someText;
    }

    public boolean isSomethingShowing(){
        return this.somethingShowing;
    }
}

3
มันใช้งานไม่ได้ ฉันได้รับ ClassCastException ... และนั่นเป็นเพราะต้องการผู้สร้างแบบคงที่สาธารณะเพื่อที่จะยกตัวอย่างของคุณStateจากพัสดุ โปรดดูที่: charlesharley.com/2012/programming/…
mato

8

คำตอบที่นี่ยอดเยี่ยมอยู่แล้ว แต่ไม่จำเป็นต้องใช้กับ ViewGroups แบบกำหนดเอง หากต้องการให้มุมมองที่กำหนดเองทั้งหมดเพื่อรักษาสถานะของพวกเขาคุณต้องแทนที่onSaveInstanceState()และonRestoreInstanceState(Parcelable state)ในแต่ละชั้นเรียน คุณต้องตรวจสอบให้แน่ใจว่าพวกเขาทั้งหมดมีรหัสที่เป็นเอกลักษณ์ไม่ว่าพวกเขาจะสูงเกินจริงจาก xml หรือเพิ่มโดยทางโปรแกรม

สิ่งที่ฉันเกิดขึ้นนั้นเป็นเหมือนคำตอบของ Kobor42 แต่ข้อผิดพลาดยังคงอยู่เพราะฉันเพิ่ม Views ไปยัง ViewGroup ที่กำหนดเองโดยทางโปรแกรมและไม่กำหนดรหัสที่ไม่ซ้ำกัน

ลิงก์ที่ใช้ร่วมกันโดย mato จะใช้งานได้ แต่หมายความว่าไม่มีการดูแต่ละรายการจัดการสถานะของตนเอง - รัฐทั้งหมดจะถูกบันทึกไว้ในวิธีการของ ViewGroup

ปัญหาคือเมื่อเพิ่ม ViewGroups เหล่านี้หลายรายการลงในเค้าโครงรหัสขององค์ประกอบจาก xml จะไม่ซ้ำกันอีกต่อไป (หากถูกกำหนดใน xml) ที่รันไทม์คุณสามารถเรียกใช้เมธอดสแตติกView.generateViewId()เพื่อรับ id เฉพาะสำหรับมุมมอง สามารถใช้ได้จาก API 17 เท่านั้น

นี่คือรหัสของฉันจาก ViewGroup (เป็นนามธรรมและ mOriginalValue เป็นตัวแปรชนิด):

public abstract class DetailRow<E> extends LinearLayout {

    private static final String SUPER_INSTANCE_STATE = "saved_instance_state_parcelable";
    private static final String STATE_VIEW_IDS = "state_view_ids";
    private static final String STATE_ORIGINAL_VALUE = "state_original_value";

    private E mOriginalValue;
    private int[] mViewIds;

// ...

    @Override
    protected Parcelable onSaveInstanceState() {

        // Create a bundle to put super parcelable in
        Bundle bundle = new Bundle();
        bundle.putParcelable(SUPER_INSTANCE_STATE, super.onSaveInstanceState());
        // Use abstract method to put mOriginalValue in the bundle;
        putValueInTheBundle(mOriginalValue, bundle, STATE_ORIGINAL_VALUE);
        // Store mViewIds in the bundle - initialize if necessary.
        if (mViewIds == null) {
            // We need as many ids as child views
            mViewIds = new int[getChildCount()];
            for (int i = 0; i < mViewIds.length; i++) {
                // generate a unique id for each view
                mViewIds[i] = View.generateViewId();
                // assign the id to the view at the same index
                getChildAt(i).setId(mViewIds[i]);
            }
        }
        bundle.putIntArray(STATE_VIEW_IDS, mViewIds);
        // return the bundle
        return bundle;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {

        // We know state is a Bundle:
        Bundle bundle = (Bundle) state;
        // Get mViewIds out of the bundle
        mViewIds = bundle.getIntArray(STATE_VIEW_IDS);
        // For each id, assign to the view of same index
        if (mViewIds != null) {
            for (int i = 0; i < mViewIds.length; i++) {
                getChildAt(i).setId(mViewIds[i]);
            }
        }
        // Get mOriginalValue out of the bundle
        mOriginalValue = getValueBackOutOfTheBundle(bundle, STATE_ORIGINAL_VALUE);
        // get super parcelable back out of the bundle and pass it to
        // super.onRestoreInstanceState(Parcelable)
        state = bundle.getParcelable(SUPER_INSTANCE_STATE);
        super.onRestoreInstanceState(state);
    } 
}

รหัสที่กำหนดเองเป็นปัญหาจริง ๆ แต่ฉันคิดว่ามันควรได้รับการจัดการเมื่อเริ่มต้นของมุมมองและไม่ได้อยู่ในสถานะบันทึก
Kobor42

จุดดี. คุณแนะนำให้ตั้งค่า mViewIds ใน Constructor แล้วเขียนทับหากมีการกู้คืนสถานะ?
ลูกธนู Johns

2

ฉันมีปัญหาที่ onRestoreInstanceState คืนค่ามุมมองที่กำหนดเองทั้งหมดของฉันด้วยสถานะของมุมมองล่าสุด ฉันแก้ไขได้โดยเพิ่มสองวิธีต่อไปนี้ในมุมมองที่กำหนดเอง:

@Override
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
    dispatchFreezeSelfOnly(container);
}

@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    dispatchThawSelfOnly(container);
}

dispatchFreezeSelfOnly และ dispatchThawSelfOnly เป็นของ ViewGroup ไม่ใช่ View ดังนั้นในกรณีที่มุมมองที่กำหนดเองของคุณถูกขยายจากมุมมองในตัว โซลูชันของคุณไม่สามารถใช้ได้
Hau Luu

1

แทนการใช้onSaveInstanceStateและคุณยังสามารถใช้onRestoreInstanceState ViewModelทำให้ตัวแบบข้อมูลของคุณขยายออกไปViewModelจากนั้นคุณสามารถใช้ViewModelProvidersเพื่อรับอินสแตนซ์เดียวกันของแบบจำลองของคุณทุกครั้งที่กิจกรรมถูกสร้างขึ้นใหม่:

class MyData extends ViewModel {
    // have all your properties with getters and setters here
}

public class MyActivity extends FragmentActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // the first time, ViewModelProvider will create a new MyData
        // object. When the Activity is recreated (e.g. because the screen
        // is rotated), ViewModelProvider will give you the initial MyData
        // object back, without creating a new one, so all your property
        // values are retained from the previous view.
        myData = ViewModelProviders.of(this).get(MyData.class);

        ...
    }
}

วิธีใช้ViewModelProvidersเพิ่มสิ่งต่อไปนี้dependenciesในapp/build.gradle:

implementation "android.arch.lifecycle:extensions:1.1.1"
implementation "android.arch.lifecycle:viewmodel:1.1.1"

โปรดทราบว่าคุณMyActivityขยายแทนเพียงการขยายFragmentActivityActivity

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับ ViewModels ได้ที่นี่:



1
@JJD ฉันเห็นด้วยกับบทความที่คุณโพสต์ยังมีการจัดการบันทึกและเรียกคืนอย่างถูกต้อง ViewModelมีประโยชน์อย่างยิ่งหากคุณมีชุดข้อมูลขนาดใหญ่ที่จะเก็บไว้ในระหว่างการเปลี่ยนสถานะเช่นการหมุนหน้าจอ ฉันชอบที่จะใช้ViewModelแทนที่จะเขียนลงในApplicationเพราะมันถูกกำหนดขอบเขตไว้อย่างชัดเจนและฉันสามารถมีหลายกิจกรรมของแอปพลิเคชันเดียวกันทำงานได้อย่างถูกต้อง
Benedikt Köppel

1

ฉันพบว่าคำตอบนี้ทำให้เกิดข้อผิดพลาดบางอย่างใน Android เวอร์ชัน 9 และ 10 ฉันคิดว่ามันเป็นวิธีการที่ดี แต่เมื่อฉันดูรหัส Androidฉันพบว่ามันหายไปจากคอนสตรัคเตอร์ คำตอบนั้นค่อนข้างเก่าดังนั้นในเวลานั้นอาจไม่จำเป็นต้องใช้มัน เมื่อฉันเพิ่ม Constructor ที่หายไปและเรียกมันจากผู้สร้างความผิดพลาดได้รับการแก้ไข

ดังนั้นนี่คือรหัสที่แก้ไข:

public class CustomView extends View {

    private int stateToSave;

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);

        // your custom state
        ss.stateToSave = this.stateToSave;

        return ss;
    }

    @Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container)
    {
        dispatchFreezeSelfOnly(container);
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());

        // your custom state
        this.stateToSave = ss.stateToSave;
    }

    @Override
    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container)
    {
        dispatchThawSelfOnly(container);
    }

    static class SavedState extends BaseSavedState {
        int stateToSave;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            this.stateToSave = in.readInt();
        }

        // This was the missing constructor
        @RequiresApi(Build.VERSION_CODES.N)
        SavedState(Parcel in, ClassLoader loader)
        {
            super(in, loader);
            this.stateToSave = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(this.stateToSave);
        }    

        public static final Creator<SavedState> CREATOR =
            new ClassLoaderCreator<SavedState>() {

            // This was also missing
            @Override
            public SavedState createFromParcel(Parcel in, ClassLoader loader)
            {
                return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ? new SavedState(in, loader) : new SavedState(in);
            }

            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in, null);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}

0

ในการเพิ่มคำตอบอื่น ๆ - หากคุณมีมุมมองผสมที่กำหนดเองหลายรายการที่มี ID เดียวกันและพวกเขาทั้งหมดจะถูกกู้คืนด้วยสถานะของมุมมองสุดท้ายในการเปลี่ยนแปลงการกำหนดค่าสิ่งที่คุณต้องทำคือบอกมุมมองให้ส่งเหตุการณ์บันทึก / กู้คืนเท่านั้น เพื่อตัวเองโดยการเอาชนะสองวิธี

class MyCompoundView : ViewGroup {

    ...

    override fun dispatchSaveInstanceState(container: SparseArray<Parcelable>) {
        dispatchFreezeSelfOnly(container)
    }

    override fun dispatchRestoreInstanceState(container: SparseArray<Parcelable>) {
        dispatchThawSelfOnly(container)
    }
}

สำหรับคำอธิบายว่าเกิดอะไรขึ้นและทำไมจึงใช้งานได้ดูโพสต์บล็อกนี้ โดยทั่วไป ID มุมมองสำหรับเด็กของมุมมองภาพรวมของคุณจะถูกแชร์โดยมุมมองภาพรวมและการคืนค่าสถานะจะทำให้เกิดความสับสน โดยการจัดส่งสถานะสำหรับมุมมองผสมเท่านั้นเราป้องกันไม่ให้ลูก ๆ ของพวกเขารับข้อความผสมจากมุมมองผสมอื่น ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.