الحفاظ على/حفظ/استعادة موضع التمرير عند العودة إلى ListView

StackOverflow https://stackoverflow.com/questions/3014089

سؤال

لدي فترة طويلة ListView يمكن للمستخدم التمرير حولها قبل العودة إلى الشاشة السابقة. عندما يفتح المستخدم هذا ListView مرة أخرى ، أريد أن يتم تمرير القائمة إلى نفس النقطة التي كانت سابقًا. أي أفكار حول كيفية تحقيق ذلك؟

هل كانت مفيدة؟

المحلول

جرب هذا:

// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());

// ...

// restore index and position
mList.setSelectionFromTop(index, top);

تفسير:

ListView.getFirstVisiblePosition() إرجاع عنصر القائمة المرئية العليا. ولكن قد يتم تمرير هذا العنصر جزئيًا عن العرض ، وإذا كنت ترغب في استعادة موضع التمرير الدقيق للقائمة التي تحتاجها للحصول على هذه الإزاحة. لذا ListView.getChildAt(0) إرجاع View للحصول على عنصر القائمة العليا ، ثم View.getTop() - mList.getPaddingTop() يعيد إزاحةه النسبي من أعلى ListView. ثم ، لاستعادة ListViewموقف التمرير ، ندعو ListView.setSelectionFromTop() مع فهرس العنصر الذي نريده وإزاحة لوضع الحافة العلوية من أعلى ListView.

نصائح أخرى

Parcelable state;

@Override
public void onPause() {    
    // Save ListView state @ onPause
    Log.d(TAG, "saving listview state @ onPause");
    state = listView.onSaveInstanceState();
    super.onPause();
}
...

@Override
public void onViewCreated(final View view, Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    // Set new items
    listView.setAdapter(adapter);
    ...
    // Restore previous state (including selected item index and scroll position)
    if(state != null) {
        Log.d(TAG, "trying to restore listview state..");
        listView.onRestoreInstanceState(state);
    }
}

لقد تبنت الحل الذي اقترحه @(Kirk Woll) ، وهو يعمل بالنسبة لي. لقد رأيت أيضًا في رمز مصدر Android لتطبيق "جهات الاتصال" ، وأنهم يستخدمون تقنية مماثلة. أرغب في إضافة بعض التفاصيل: في الأعلى على فئتي المشتقة من القائمة:

private static final String LIST_STATE = "listState";
private Parcelable mListState = null;

ثم ، تتجاوز بعض الطرق:

@Override
protected void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    mListState = state.getParcelable(LIST_STATE);
}

@Override
protected void onResume() {
    super.onResume();
    loadData();
    if (mListState != null)
        getListView().onRestoreInstanceState(mListState);
    mListState = null;
}

@Override
protected void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    mListState = getListView().onSaveInstanceState();
    state.putParcelable(LIST_STATE, mListState);
}

بالطبع "LoadData" هي وظيفتي لاسترداد البيانات من DB ووضعها على القائمة.

على جهاز Froyo الخاص بي ، يعمل هذا عند تغيير اتجاه الهاتف ، وعندما تقوم بتحرير عنصر وتعود إلى القائمة.

طريقة بسيطة جدا:

/** Save the position **/
int currentPosition = listView.getFirstVisiblePosition();

//Here u should save the currentPosition anywhere

/** Restore the previus saved position **/
listView.setSelection(savedPosition);

طريقة setSelection سيتم إعادة تعيين القائمة إلى العنصر المقدم. إذا لم يكن ذلك في وضع اللمس ، فسيتم تحديد العنصر فعليًا إذا تم وضع العنصر في وضع اللمس فقط على الشاشة.

نهج أكثر تعقيدًا:

listView.setOnScrollListener(this);

//Implements the interface:
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
            int visibleItemCount, int totalItemCount) {
    mCurrentX = view.getScrollX();
    mCurrentY = view.getScrollY();
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {

}

//Save anywere the x and the y

/** Restore: **/
listView.scrollTo(savedX, savedY);

لقد وجدت شيئًا مثيرًا للاهتمام حول هذا الموضوع.

حاولت SetSelection و ScrollToxy لكنها لم تنجح على الإطلاق ، بقيت القائمة في نفس الموقف ، بعد بعض التجربة والخطأ ، حصلت على الرمز التالي الذي يعمل

final ListView list = (ListView) findViewById(R.id.list);
list.post(new Runnable() {            
    @Override
    public void run() {
        list.setSelection(0);
    }
});

إذا كانت بدلاً من نشر Runnable تحاول RunonuithRead ، فهذا لا يعمل أيضًا (على الأقل على بعض الأجهزة)

هذا حل غريب للغاية لشيء يجب أن يكون مستقيمًا للأمام.

الحذر!! هناك خطأ في AbstView لا يسمح لـ OnSavestate () بالعمل بشكل صحيح إذا كان ListView.getFirstVisiblePosition () هو 0.

لذا ، إذا كان لديك صور كبيرة تتناول معظم الشاشة ، وقمت بالتمرير إلى الصورة الثانية ، ولكن القليل من الأول يظهر ، فلن يتم حفظ موضع التمرير ...

من عند absistview.java:1650 (تعليقات الألغام)

// this will be false when the firstPosition IS 0
if (haveChildren && mFirstPosition > 0) {
    ...
} else {
    ss.viewTop = 0;
    ss.firstId = INVALID_POSITION;
    ss.position = 0;
}

ولكن في هذه الحالة ، سيكون "القمة" في الكود أدناه رقمًا سالبًا الذي يسبب مشكلات أخرى تمنع استعادة الدولة بشكل صحيح. لذلك عندما يكون "الأعلى" سلبيًا ، احصل على الطفل التالي

// save index and top position
int index = getFirstVisiblePosition();
View v = getChildAt(0);
int top = (v == null) ? 0 : v.getTop();

if (top < 0 && getChildAt(1) != null) {
    index++;
    v = getChildAt(1);
    top = v.getTop();
}
// parcel the index and top

// when restoring, unparcel index and top
listView.setSelectionFromTop(index, top);
private Parcelable state;
@Override
public void onPause() {
    state = mAlbumListView.onSaveInstanceState();
    super.onPause();
}

@Override
public void onResume() {
    super.onResume();

    if (getAdapter() != null) {
        mAlbumListView.setAdapter(getAdapter());
        if (state != null){
            mAlbumListView.requestFocus();
            mAlbumListView.onRestoreInstanceState(state);
        }
    }
}

هذا يكفي

بالنسبة للبعض يبحث عن حل لهذه المشكلة ، قد يكون جذر المشكلة هو المكان الذي تقوم فيه بتعيين محول طرق عرض القائمة. بعد تعيين المحول على ListView ، تقوم بإعادة تعيين موضع التمرير. مجرد شيء للنظر فيه. لقد قمت بنقل المحول إلى onCreateview الخاص بي بعد أن تمسكنا بالإشارة إلى ListView ، وحل المشكلة بالنسبة لي. =)

أنا أنشر هذا لأنني مندهش لم يذكر أحد هذا.

بعد أن ينقر المستخدم على زر العودة ، سيعود إلى ListView في نفس الحالة التي خرج منها.

سيقوم هذا الرمز بتجاوز الزر "UP" للتصرف بنفس الطريقة التي يتصرف بها زر العودة ، لذا في حالة ListView -> التفاصيل -> العودة إلى ListView (وليس هناك خيارات أخرى) هذا هو أبسط رمز للحفاظ على التمرير والمحتوى في ListView.

 public boolean onOptionsItemSelected(MenuItem item) {
     switch (item.getItemId()) {
         case android.R.id.home:
             onBackPressed();
             return(true);
     }
     return(super.onOptionsItemSelected(item)); }

تحذير: إذا تمكنت من الانتقال إلى نشاط آخر من نشاط التفاصيل ، فسيعودك زر UP إلى هذا النشاط حتى تضطر إلى معالجة تاريخ Backbutton حتى يعمل هذا.

ليس ببساطة android:saveEnabled="true" في إعلان ListView XML بما فيه الكفاية؟

أفضل حل هو:

// save index and top position
int index = mList.getFirstVisiblePosition();
View v = mList.getChildAt(0);
int top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());

// ...

// restore index and position
mList.post(new Runnable() {
    @Override
    public void run() {
      mList.setSelectionFromTop(index, top);
   }
});

يجب عليك الاتصال في المنشور وفي الموضوع!

يمكنك الحفاظ على حالة التمرير بعد إعادة التحميل إذا قمت بحفظ الدولة قبل إعادة تحميلها واستعادتها بعد. في حالتي ، قدمت طلب شبكة غير متزامن وأعيد تحميل القائمة في رد اتصال بعد اكتماله. هذا هو المكان الذي استعدت فيه الدولة. عينة الكود هي Kotlin.

val state = myList.layoutManager.onSaveInstanceState()

getNewThings() { newThings: List<Thing> ->

    myList.adapter.things = newThings
    myList.layoutManager.onRestoreInstanceState(state)
}

إذا كنت تستخدم شظايا مستضافة على نشاط ، يمكنك القيام بشيء مثل هذا:

public abstract class BaseFragment extends Fragment {
     private boolean mSaveView = false;
     private SoftReference<View> mViewReference;

     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
          if (mSaveView) {
               if (mViewReference != null) {
                    final View savedView = mViewReference.get();
                    if (savedView != null) {
                         if (savedView.getParent() != null) {
                              ((ViewGroup) savedView.getParent()).removeView(savedView);
                              return savedView;
                         }
                    }
               }
          }

          final View view = inflater.inflate(getFragmentResource(), container, false);
          mViewReference = new SoftReference<View>(view);
          return view;
     }

     protected void setSaveView(boolean value) {
           mSaveView = value;
     }
}

public class MyFragment extends BaseFragment {
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
          setSaveView(true);
          final View view = super.onCreateView(inflater, container, savedInstanceState);
          ListView placesList = (ListView) view.findViewById(R.id.places_list);
          if (placesList.getAdapter() == null) {
               placesList.setAdapter(createAdapter());
          }
     }
}

إذا كنت تنقذ/استعادة موقف التمرير ListView أنت تقوم بتكرار الوظائف التي تم تنفيذها بالفعل في Framework Android. ال ListView يعيد وضع التمرير الجيد جيدًا من تلقاء نفسه باستثناء تحذير واحد: كما ذكر Aaronvargas هناك خطأ في AbsListView لن يسمح ذلك لاستعادة موضع التمرير الجيد لعنصر القائمة الأولى. ومع ذلك ، فإن أفضل طريقة لاستعادة موقف التمرير هي عدم استعادته. سوف يقوم Android Framework بعمله بشكل أفضل بالنسبة لك. فقط تأكد من استوفى الشروط التالية:

  • تأكد من أنك لم تتصل setSaveEnabled(false) الطريقة وليس تعيينها android:saveEnabled="false" سمة للحصول على القائمة في ملف تخطيط XML
  • ل ExpandableListView تجاوز long getCombinedChildId(long groupId, long childId) الطريقة بحيث تُرجع رقم طويل إيجابي (التنفيذ الافتراضي في الفصل BaseExpandableListAdapter إرجاع الرقم السلبي). فيما يلي أمثلة:

.

@Override
public long getChildId(int groupPosition, int childPosition) {
    return 0L | groupPosition << 12 | childPosition;
}

@Override
public long getCombinedChildId(long groupId, long childId) {
    return groupId << 32 | childId << 1 | 1;
}

@Override
public long getGroupId(int groupPosition) {
    return groupPosition;
}

@Override
public long getCombinedGroupId(long groupId) {
    return (groupId & 0x7FFFFFFF) << 32;
}
  • لو ListView أو ExpandableListView يستخدم في جزء لا يعيد إنشاء جزء على استجمام النشاط (بعد دوران الشاشة على سبيل المثال). الحصول على الشظية مع findFragmentByTag(String tag) طريقة.
  • تأكد من أن ListView لديه android:id وهو فريد من نوعه.

لتجنب التحذير المذكور أعلاه مع عنصر القائمة الأولى ، يمكنك صياغة محولك بالطريقة التي تُرجع بها عرض ارتفاع البكسلات الصفرية الوهمية الخاصة بـ ListView في الموضع 0. هنا يظهر المشروع المثال البسيط ListView و ExpandableListView استعادة مواقف التمرير الجميلة الخاصة بهم في حين أن مواقع التمرير الخاصة بهم لا يتم حفظها/استعادةها بشكل صريح. تتم استعادة موضع التمرير الدقيق تمامًا حتى بالنسبة للسيناريوهات المعقدة مع التبديل المؤقت إلى بعض التطبيقات الأخرى ، وتناوب الشاشة المزدوجة والعودة إلى تطبيق الاختبار. يرجى ملاحظة أنه إذا كنت تخرج بشكل صريح من التطبيق (عن طريق الضغط على زر العودة) ، فلن يتم حفظ موضع التمرير (وكذلك جميع المشاهدات الأخرى لن تنقذ حالتها).https://github.com/voromto/restorescrollposition/releases

بالنسبة للنشاط المستمد من القائمة التي تنفذ loadermanager.loaderCallbacks باستخدام simpleCursorAdapter ، لم ينجح في استعادة الموقف في onreset () ، لأنه تم إعادة تشغيل النشاط تقريبًا وتم إعادة تحميل المحول عند إغلاق عرض التفاصيل. كانت الحيلة هي استعادة الموقف في onloadfinished ():

في onlistitemclick ():

// save the selected item position when an item was clicked
// to open the details
index = getListView().getFirstVisiblePosition();
View v = getListView().getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - getListView().getPaddingTop());

في onloadfinish ():

// restore the selected item which was saved on item click
// when details are closed and list is shown again
getListView().setSelectionFromTop(index, top);

في onbackpressed ():

// Show the top item at next start of the app
index = 0;
top = 0;

لم يكن أي من الحلول المقدمة هنا يعمل من أجلي. في حالتي ، لدي ملف ListView في Fragment الذي استبدله في FragmentTransaction, ، لذلك جديد Fragment يتم إنشاء مثيل في كل مرة يتم عرض الجزء ، مما يعني أن ListView لا يمكن تخزين الدولة كعضو في Fragment.

بدلاً من ذلك ، انتهى بي الأمر بتخزين الحالة في عادتي Application صف دراسي. يجب أن يعطيك الرمز أدناه فكرة عن كيفية عمل هذا:

public class MyApplication extends Application {
    public static HashMap<String, Parcelable> parcelableCache = new HashMap<>();


    /* ... code omitted for brevity ... */
}

 

public class MyFragment extends Fragment{
    private ListView mListView = null;
    private MyAdapter mAdapter = null;


    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mAdapter = new MyAdapter(getActivity(), null, 0);
        mListView = ((ListView) view.findViewById(R.id.myListView));

        Parcelable listViewState = MyApplication.parcelableCache.get("my_listview_state");
        if( listViewState != null )
            mListView.onRestoreInstanceState(listViewState);
    }


    @Override
    public void onPause() {
        MyApplication.parcelableCache.put("my_listview_state", mListView.onSaveInstanceState());
        super.onPause();
    }

    /* ... code omitted for brevity ... */

}

الفكرة الأساسية هي أنك تخزن الحالة خارج مثيل الجزء. إذا كنت لا تحب فكرة وجود حقل ثابت في فصل التطبيق الخاص بك ، أعتقد أنه يمكنك القيام بذلك عن طريق تطبيق واجهة جزء وتخزين الحالة في نشاطك.

حل آخر هو تخزينه في SharedPreferences, ، لكنه يصبح أكثر تعقيدًا بعض الشيء ، وستحتاج إلى التأكد من مسحه عند إطلاق التطبيق إلا إذا كنت تريد أن تظل الدولة في عمليات إطلاق التطبيقات.

 

أيضًا ، لتجنب "موضع التمرير الذي لم يتم حفظه عندما يكون العنصر الأول مرئيًا" ، يمكنك عرض عنصر أول وهمية مع 0px ارتفاع. يمكن تحقيق ذلك عن طريق تجاوز getView() في محولك ، مثل هذا:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if( position == 0 ) {
        View zeroHeightView = new View(parent.getContext());
        zeroHeightView.setLayoutParams(new ViewGroup.LayoutParams(0, 0));
        return zeroHeightView;
    }
    else
        return super.getView(position, convertView, parent);
}

إجابتي هي لـ Firebase والموقف 0 هو حل بديل

Parcelable state;

DatabaseReference everybody = db.getReference("Everybody Room List");
    everybody.addValueEventListener(new ValueEventListener() {
        @Override
        public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
            state = listView.onSaveInstanceState(); // Save
            progressBar.setVisibility(View.GONE);
            arrayList.clear();
            for (DataSnapshot messageSnapshot : dataSnapshot.getChildren()) {
                Messages messagesSpacecraft = messageSnapshot.getValue(Messages.class);
                arrayList.add(messagesSpacecraft);
            }
            listView.setAdapter(convertView);
            listView.onRestoreInstanceState(state); // Restore
        }

        @Override
        public void onCancelled(@NonNull DatabaseError databaseError) {
        }
    });

و ConvertView

ضع 0 A إضافة عنصر فارغ لا تستخدمه

public class Chat_ConvertView_List_Room extends BaseAdapter {

private ArrayList<Messages> spacecrafts;
private Context context;

@SuppressLint("CommitPrefEdits")
Chat_ConvertView_List_Room(Context context, ArrayList<Messages> spacecrafts) {
    this.context = context;
    this.spacecrafts = spacecrafts;
}

@Override
public int getCount() {
    return spacecrafts.size();
}

@Override
public Object getItem(int position) {
    return spacecrafts.get(position);
}

@Override
public long getItemId(int position) {
    return position;
}

@SuppressLint({"SetTextI18n", "SimpleDateFormat"})
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = LayoutInflater.from(context).inflate(R.layout.message_model_list_room, parent, false);
    }

    final Messages s = (Messages) this.getItem(position);

    if (position == 0) {
        convertView.getLayoutParams().height = 1; // 0 does not work
    } else {
        convertView.getLayoutParams().height = RelativeLayout.LayoutParams.WRAP_CONTENT;
    }

    return convertView;
}
}

لقد رأيت هذا العمل مؤقتًا دون إزعاج المستخدم ، وآمل أن يناسبك

استخدم هذا الرمز أدناه:

int index,top;

@Override
protected void onPause() {
    super.onPause();
    index = mList.getFirstVisiblePosition();

    View v = challengeList.getChildAt(0);
    top = (v == null) ? 0 : (v.getTop() - mList.getPaddingTop());
}

وكلما تحديث بياناتك ، استخدم هذا الرمز أدناه:

adapter.notifyDataSetChanged();
mList.setSelectionFromTop(index, top);

أنا استخدم Firebaselistadapter ولم يستطع الحصول على أي من الحلول للعمل. انتهى بي الأمر القيام بذلك. أظن أن هناك طرقًا أكثر أناقة ولكن هذا حل كامل وعمل.

قبل oncreate:

private int reset;
private int top;
private int index;

داخل Firebaselistadapter:

@Override
public void onDataChanged() {
     super.onDataChanged();

     // Only do this on first change, when starting
     // activity or coming back to it.
     if(reset == 0) {
          mListView.setSelectionFromTop(index, top);
          reset++;
     }

 }

onstart:

@Override
protected void onStart() {
    super.onStart();
    if(adapter != null) {
        adapter.startListening();
        index = 0;
        top = 0;
        // Get position from SharedPrefs
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        top = sharedPref.getInt("TOP_POSITION", 0);
        index = sharedPref.getInt("INDEX_POSITION", 0);
        // Set reset to 0 to allow change to last position
        reset = 0;
    }
}

onstop:

@Override
protected void onStop() {
    super.onStop();
    if(adapter != null) {
        adapter.stopListening();
        // Set position
        index = mListView.getFirstVisiblePosition();
        View v = mListView.getChildAt(0);
        top = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop());
        // Save position to SharedPrefs
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        sharedPref.edit().putInt("TOP_POSITION" + "", top).apply();
        sharedPref.edit().putInt("INDEX_POSITION" + "", index).apply();
    }
}

منذ أن اضطررت أيضًا إلى حل هذا Firebaserecycleradapter أقوم بنشر الحل هنا أيضًا:

قبل oncreate:

private int reset;
private int top;
private int index;

داخل firebaserecycleradapter:

@Override
public void onDataChanged() {
    // Only do this on first change, when starting
    // activity or coming back to it.
    if(reset == 0) {
        linearLayoutManager.scrollToPositionWithOffset(index, top);
        reset++;
    }
}

onstart:

@Override
protected void onStart() {
    super.onStart();
    if(adapter != null) {
        adapter.startListening();
        index = 0;
        top = 0;
        // Get position from SharedPrefs
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        top = sharedPref.getInt("TOP_POSITION", 0);
        index = sharedPref.getInt("INDEX_POSITION", 0);
        // Set reset to 0 to allow change to last position
        reset = 0;
    }
}

onstop:

@Override
protected void onStop() {
    super.onStop();
    if(adapter != null) {
        adapter.stopListening();
        // Set position
        index = linearLayoutManager.findFirstVisibleItemPosition();
        View v = linearLayoutManager.getChildAt(0);
        top = (v == null) ? 0 : (v.getTop() - linearLayoutManager.getPaddingTop());
        // Save position to SharedPrefs
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        sharedPref.edit().putInt("TOP_POSITION" + "", top).apply();
        sharedPref.edit().putInt("INDEX_POSITION" + "", index).apply();
    }
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top