Programing

RecyclerView.State를 사용하여 RecyclerView의 스크롤 위치를 저장하는 방법은 무엇입니까?

lottogame 2020. 8. 27. 08:05
반응형

RecyclerView.State를 사용하여 RecyclerView의 스크롤 위치를 저장하는 방법은 무엇입니까?


Android의 RecyclerView.State 에 대한 질문이 있습니다 .

RecyclerView를 사용하고 있는데 RecyclerView.State와 어떻게 사용하고 바인딩 할 수 있습니까?

내 목적은 RecyclerView의 스크롤 위치저장하는 것입니다 .


마지막으로 저장된 위치를 RecyclerView.State어떻게 저장할 계획 입니까?

항상 좋은 저장 상태에 의존 할 수 있습니다. RecyclerViewonSaveInstanceState ()를 확장 하고 재정의합니다 onRestoreInstanceState().

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        LayoutManager layoutManager = getLayoutManager();
        if(layoutManager != null && layoutManager instanceof LinearLayoutManager){
            mScrollPosition = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
        }
        SavedState newState = new SavedState(superState);
        newState.mScrollPosition = mScrollPosition;
        return newState;
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        if(state != null && state instanceof SavedState){
            mScrollPosition = ((SavedState) state).mScrollPosition;
            LayoutManager layoutManager = getLayoutManager();
            if(layoutManager != null){
              int count = layoutManager.getItemCount();
              if(mScrollPosition != RecyclerView.NO_POSITION && mScrollPosition < count){
                  layoutManager.scrollToPosition(mScrollPosition);
              }
            }
        }
    }

    static class SavedState extends android.view.View.BaseSavedState {
        public int mScrollPosition;
        SavedState(Parcel in) {
            super(in);
            mScrollPosition = in.readInt();
        }
        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeInt(mScrollPosition);
        }
        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

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

LinearLayoutManager를 사용하는 경우 미리 빌드 된 저장 API linearLayoutManagerInstance.onSaveInstanceState () 및 복원 api linearLayoutManagerInstance.onRestoreInstanceState (...)가 함께 제공됩니다.

이를 통해 반환 된 parcelable을 outState에 저장할 수 있습니다. 예 :

outState.putParcelable("KeyForLayoutManagerState", linearLayoutManagerInstance.onSaveInstanceState());

, 저장 한 상태로 복원 위치를 복원합니다. 예 :

Parcelable state = savedInstanceState.getParcelable("KeyForLayoutManagerState");
linearLayoutManagerInstance.onRestoreInstanceState(state);

모두 마무리하면 최종 코드는 다음과 같습니다.

private static final String BUNDLE_RECYCLER_LAYOUT = "classname.recycler.layout";

/**
 * This is a method for Fragment. 
 * You can do the same in onCreate or onRestoreInstanceState
 */
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
    super.onViewStateRestored(savedInstanceState);

    if(savedInstanceState != null)
    {
        Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        recyclerView.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
    }
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, recyclerView.getLayoutManager().onSaveInstanceState());
}

편집 : LinearLayoutManager의 하위 클래스이기 때문에 GridLayoutManager와 동일한 API를 사용할 수도 있습니다. 제안에 대해 @wegsehen에게 감사드립니다.

편집 : 백그라운드 스레드에서도 데이터를로드하는 경우 방향 변경시 위치를 복원하려면 onPostExecute / onLoadFinished 메서드 내에서 onRestoreInstanceState를 호출해야합니다. 예 :

@Override
protected void onPostExecute(ArrayList<Movie> movies) {
    mLoadingIndicator.setVisibility(View.INVISIBLE);
    if (movies != null) {
        showMoviePosterDataView();
        mDataAdapter.setMovies(movies);
      mRecyclerView.getLayoutManager().onRestoreInstanceState(mSavedRecyclerLayoutState);
        } else {
            showErrorMessage();
        }
    }

저장

lastFirstVisiblePosition = ((LinearLayoutManager)rv.getLayoutManager()).findFirstCompletelyVisibleItemPosition();

복원

((LinearLayoutManager) rv.getLayoutManager()).scrollToPosition(lastFirstVisiblePosition);

그래도 작동하지 않으면

((LinearLayoutManager) rv.getLayoutManager()).scrollToPositionWithOffset(lastFirstVisiblePosition,0)

저장 onPause()및 복원onResume()


내 목록 활동에서 멀리 이동할 때 Recycler View의 스크롤 위치를 저장하고 뒤로 이동하기 위해 뒤로 버튼을 클릭하고 싶었습니다. 이 문제에 대해 제공된 많은 솔루션이 필요 이상으로 복잡하거나 내 구성에서 작동하지 않으므로 솔루션을 공유 할 것이라고 생각했습니다.

먼저 많은 사람들이 보여준대로 onPause에 인스턴스 상태를 저장합니다. 이 버전의 onSaveInstanceState가 RecyclerView.LayoutManager 클래스의 메서드라는 점을 여기서 강조 할 가치가 있다고 생각합니다.

private LinearLayoutManager mLayoutManager;
Parcelable state;

        @Override
        public void onPause() {
            super.onPause();
            state = mLayoutManager.onSaveInstanceState();
        }

이것이 제대로 작동하도록하는 핵심은 어댑터를 연결 한 후 onRestoreInstanceState를 호출하는 것입니다. 일부는 다른 스레드에 표시되어 있습니다. 그러나 실제 메서드 호출은 많은 사람들이 표시 한 것보다 훨씬 간단합니다.

private void someMethod() {
    mVenueRecyclerView.setAdapter(mVenueAdapter);
    mLayoutManager.onRestoreInstanceState(state);
}

onCreate ()에 변수를 설정하고 onPause ()에 스크롤 위치를 저장하고 onResume ()에 스크롤 위치를 설정합니다.

public static int index = -1;
public static int top = -1;
LinearLayoutManager mLayoutManager;

@Override
public void onCreate(Bundle savedInstanceState)
{
    //Set Variables
    super.onCreate(savedInstanceState);
    cRecyclerView = ( RecyclerView )findViewById(R.id.conv_recycler);
    mLayoutManager = new LinearLayoutManager(this);
    cRecyclerView.setHasFixedSize(true);
    cRecyclerView.setLayoutManager(mLayoutManager);

}

@Override
public void onPause()
{
    super.onPause();
    //read current recyclerview position
    index = mLayoutManager.findFirstVisibleItemPosition();
    View v = cRecyclerView.getChildAt(0);
    top = (v == null) ? 0 : (v.getTop() - cRecyclerView.getPaddingTop());
}

@Override
public void onResume()
{
    super.onResume();
    //set recyclerview position
    if(index != -1)
    {
        mLayoutManager.scrollToPositionWithOffset( index, top);
    }
}

더 이상 스스로 상태를 저장하고 복원 할 필요가 없습니다. xml에서 고유 ID를 설정하고 recyclerView.setSaveEnabled (true) (기본값은 true) 시스템이 자동으로이를 수행합니다. 이에 대한 자세한 내용은 http://trickyandroid.com/saving-android-view-state-correctly/입니다.


AsyncTaskLoader를 사용하여 인터넷에서 데이터를 다시로드해야 할 때 회전 후 GridLayoutManager로 RecyclerView 위치를 복원하는 방법입니다.

Parcelable 및 GridLayoutManager의 전역 변수와 정적 최종 문자열을 만듭니다.

private Parcelable savedRecyclerLayoutState;
private GridLayoutManager mGridLayoutManager;
private static final String BUNDLE_RECYCLER_LAYOUT = "recycler_layout";

onSaveInstance ()에 gridLayoutManager 상태 저장

 @Override
        protected void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            outState.putParcelable(BUNDLE_RECYCLER_LAYOUT, 
            mGridLayoutManager.onSaveInstanceState());
        }

onRestoreInstanceState에서 복원

@Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);

        //restore recycler view at same position
        if (savedInstanceState != null) {
            savedRecyclerLayoutState = savedInstanceState.getParcelable(BUNDLE_RECYCLER_LAYOUT);
        }
    }

그런 다음 로더가 인터넷에서 데이터를 가져올 때 onLoadFinished ()에서 recyclerview 위치를 복원합니다.

if(savedRecyclerLayoutState!=null){
                mGridLayoutManager.onRestoreInstanceState(savedRecyclerLayoutState);
            }

.. 물론 onCreate 내에서 gridLayoutManager를 인스턴스화해야합니다. 건배


지원 라이브러리에 번들로 제공되는 모든 레이아웃 관리자는 스크롤 위치를 저장하고 복원하는 방법을 이미 알고 있습니다.

스크롤 위치는 다음 조건이 충족 될 때 발생하는 첫 번째 레이아웃 단계에서 복원됩니다.

  • 레이아웃 관리자는 RecyclerView
  • adpater는 RecyclerView

일반적으로 레이아웃 관리자를 XML로 설정하므로 지금해야 할 일은

  1. 데이터와 함께 어댑터로드
  2. 그런 다음 어댑터를RecyclerView

이 작업은 언제든지 수행 할 수 있으며 Fragment.onCreateView또는로 제한되지 않습니다 Activity.onCreate.

예 : 데이터가 업데이트 될 때마다 어댑터가 연결되어 있는지 확인하십시오.

viewModel.liveData.observe(this) {
    // Load adapter.
    adapter.data = it

    if (list.adapter != adapter) {
        // Only set the adapter if we didn't do it already.
        list.adapter = adapter
    }
}

저장된 스크롤 위치는 다음 데이터에서 계산됩니다.

  • 화면에서 첫 번째 항목의 어댑터 위치
  • 목록 상단에서 항목 상단의 픽셀 오프셋 (세로 레이아웃의 경우)

따라서 항목의 크기가 세로 및 가로 방향이 다른 경우 약간의 불일치를 예상 할 수 있습니다.


여기에 그것들을 저장하고 조각에서 recyclerview의 상태를 유지하는 방법이 있습니다. 먼저 아래 코드와 같이 상위 활동에 구성 변경 사항을 추가하십시오.

android:configChanges="orientation|screenSize"

그런 다음 Fragment에서 이러한 인스턴스 메서드의

private  Bundle mBundleRecyclerViewState;
private Parcelable mListState = null;
private LinearLayoutManager mLinearLayoutManager;

@onPause 메서드에 아래 코드를 추가하십시오.

@Override
public void onPause() {
    super.onPause();
    //This used to store the state of recycler view
    mBundleRecyclerViewState = new Bundle();
    mListState =mTrailersRecyclerView.getLayoutManager().onSaveInstanceState();
    mBundleRecyclerViewState.putParcelable(getResources().getString(R.string.recycler_scroll_position_key), mListState);
}

아래와 같이 onConfigartionChanged Method를 추가합니다.

@Override
public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    //When orientation is changed then grid column count is also changed so get every time
    Log.e(TAG, "onConfigurationChanged: " );
    if (mBundleRecyclerViewState != null) {
        new Handler().postDelayed(new Runnable() {

            @Override
            public void run() {
                mListState = mBundleRecyclerViewState.getParcelable(getResources().getString(R.string.recycler_scroll_position_key));
                mTrailersRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);

            }
        }, 50);
    }
    mTrailersRecyclerView.setLayoutManager(mLinearLayoutManager);
}

이 접근 방식은 문제를 다시 해결할 것입니다. 다른 레이아웃 관리자가 있으면 상위 레이아웃 관리자를 교체하십시오.


나에게 문제는 어댑터를 변경할 때마다 새 레이아웃 관리자를 설정하여 recyclerView에서 스크롤 위치를 잃었다는 것입니다.


On Android API Level 28, I simply ensure that I set up my LinearLayoutManager and RecyclerView.Adapter in my Fragment#onCreateView method, and everything Just Worked™️. I didn't need to do any onSaveInstanceState or onRestoreInstanceState work.

Eugen Pechanec's answer explains why this works.


I would just like to share the recent predicament I encounter with the RecyclerView. I hope that anyone experiencing the same problem will benefit.

My Project Requirement: So I have a RecyclerView that list some clickable items in my Main Activity (Activity-A). When the Item is clicked a new Activity is shown with the Item Details (Activity-B).

I implemented in the Manifest file the that the Activity-A is the parent of Activity-B, that way, I have a back or home button on the ActionBar of the Activity-B

Problem: Every time I pressed the Back or Home button in the Activity-B ActionBar, the Activity-A goes into the full Activity Life Cycle starting from onCreate()

Even though I implemented an onSaveInstanceState() saving the List of the RecyclerView's Adapter, when Activity-A starts it's lifecycle, the saveInstanceState is always null in the onCreate() method.

Further digging in the internet, I came across the same problem but the person noticed that the Back or Home button below the Anroid device (Default Back/Home button), the Activity-A does not goes into the Activity Life-Cycle.

Solution:

  1. I removed the Tag for Parent Activity in the manifest for Activity-B
  2. I enabled the home or back button on Activity-B

    Under onCreate() method add this line supportActionBar?.setDisplayHomeAsUpEnabled(true)

  3. In the overide fun onOptionsItemSelected() method, I checked for the item.ItemId on which item is clicked based on the id. The Id for the back button is

    android.R.id.home

    Then implement a finish() function call inside the android.R.id.home

    This will end the Activity-B and bring Acitivy-A without going through the entire life-cycle.

For my requirement this is the best solution so far.

     override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_show_project)

    supportActionBar?.title = projectName
    supportActionBar?.setDisplayHomeAsUpEnabled(true)

}


 override fun onOptionsItemSelected(item: MenuItem?): Boolean {

    when(item?.itemId){

        android.R.id.home -> {
            finish()
        }
    }

    return super.onOptionsItemSelected(item)
}

I've had the same requirement, but the solutions here didn't quite get me across the line, due to the source of data for the recyclerView.

I was extracting the RecyclerViews' LinearLayoutManager state in onPause()

private Parcelable state;

Public void onPause() {
    state = mLinearLayoutManager.onSaveInstanceState();
}

Parcelable state is saved in onSaveInstanceState(Bundle outState), and extracted again in onCreate(Bundle savedInstanceState), when savedInstanceState != null.

However, the recyclerView adapter is populated and updated by a ViewModel LiveData object returned by a DAO call to a Room database.

mViewModel.getAllDataToDisplay(mSelectedData).observe(this, new Observer<List<String>>() {
    @Override
    public void onChanged(@Nullable final List<String> displayStrings) {
        mAdapter.swapData(displayStrings);
        mLinearLayoutManager.onRestoreInstanceState(state);
    }
});

I eventually found that restoring the instance state directly after the data is set in the adapter would keep the scroll-state across rotations.

I suspect this either is because the LinearLayoutManager state that I'd restored was being overwritten when the data was returned by the database call, or the restored state was meaningless against an 'empty' LinearLayoutManager.

If the adapter data is available directly (ie not contigent on a database call), then restoring the instance state on the LinearLayoutManager can be done after the adapter is set on the recyclerView.

The distinction between the two scenarios held me up for ages.


I had the same problem with a minor difference, I was using Databinding, and I was setting recyclerView adapter and the layoutManager in the binding methods.

The problem was that data-binding was happening after onResume so it was resetting my layoutManager after onResume and that means it was scrolling back to original place every time. So when I stopped setting layoutManager in Databinding adaptor methods, it works fine again.


Activity.java:

public RecyclerView.LayoutManager mLayoutManager;

Parcelable state;

    mLayoutManager = new LinearLayoutManager(this);


// Inside `onCreate()` lifecycle method, put the below code :

    if(state != null) {
    mLayoutManager.onRestoreInstanceState(state);

    } 

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

        if (state != null) {
            mLayoutManager.onRestoreInstanceState(state);
        }
    }   

   @Override
    protected void onPause() {
        super.onPause();

        state = mLayoutManager.onSaveInstanceState();

    }   

Why I'm using OnSaveInstanceState() in onPause() means, While switch to another activity onPause would be called.It will save that scroll position and restore the position when we coming back from another activity.

참고URL : https://stackoverflow.com/questions/27816217/how-to-save-recyclerviews-scroll-position-using-recyclerview-state

반응형