在使用 Firebase 和 ChildEventListener 时,如何在方向更改后保留 RecyclerView 的位置?

Posted

技术标签:

【中文标题】在使用 Firebase 和 ChildEventListener 时,如何在方向更改后保留 RecyclerView 的位置?【英文标题】:How to retain RecyclerView's position after Orientation change, while using Firebase & ChildEventListener? 【发布时间】:2017-07-19 17:37:44 【问题描述】:

我正在开发一个简单的 APOD 应用程序,它实现了:

RecyclerView CardView Firebase Picasso

应用程序从FirebaseFirebase Storage 中获取图像和文本,将它们显示在CardView 中,并为每个View 设置一个OnClickListener。当用户单击图像时,我通过Intent 打开一个新的Activity。第二个Activity 显示原始点击图像,以及有关它的更多信息。

我已经使用GridLayoutManager 实现了这一切,如果用户的手机是垂直的,则为 1 列,如果用户的手机是水平的,则为 3 列。

我遇到的问题是我似乎无法保存RecyclerView 在方向更改时的位置。我已经尝试了我能找到的每一个选项,但似乎没有一个有效。我能得出的唯一结论是,在轮换时,我正在销毁FirebaseChildEventListener 以避免内存泄漏,一旦定位完成,Firebase 将重新查询数据库,因为新的ChildEventListener 的实例。

有什么方法可以保存我的RecyclerView 在方向更改时的位置?我不想要android:configChanges,因为它不允许我更改布局,而且我已经尝试保存为Parcelable,但没有成功。我敢肯定我错过了一些简单的事情,但是嘿,我是开发新手。非常感谢对我的代码的任何帮助或建议。谢谢!

以下是我的课程,我已将其缩短为必要的代码。

MainActivity

public class MainActivity extends AppCompatActivity 

private RecyclerAdapter mRecyclerAdapter;
private DatabaseReference mDatabaseReference;
private RecyclerView mRecyclerView;
private Query query;


@Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);

    PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
    setContentView(R.layout.activity_main);
    getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    final int columns = getResources().getInteger(R.integer.gallery_columns);

    mDatabaseReference = FirebaseDatabase.getInstance().getReference();
    query = mDatabaseReference.child("apod").orderByChild("date");

    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    mRecyclerView.setHasFixedSize(true);
    mRecyclerView.setLayoutManager(new GridLayoutManager(this, columns));


    mRecyclerAdapter = new RecyclerAdapter(this, query);
    mRecyclerView.setAdapter(mRecyclerAdapter);


  

@Override
public void onDestroy() 
    mRecyclerAdapter.cleanupListener();
  


RecyclerAdapter

public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ApodViewHolder> 

private final Context mContext;
private final ChildEventListener mChildEventListener;
private final Query mDatabaseReference;
private final List<String> apodListIds = new ArrayList<>();
private final List<Apod> apodList = new ArrayList<>();

public RecyclerAdapter(final Context context, Query ref) 
    mContext = context;
    mDatabaseReference = ref;


    ChildEventListener childEventListener = new ChildEventListener() 


        @Override
        public void onChildAdded(DataSnapshot dataSnapshot, String s) 
            int oldListSize = getItemCount();
            Apod apod = dataSnapshot.getValue(Apod.class);

            //Add data and IDs to the list
            apodListIds.add(dataSnapshot.getKey());
            apodList.add(apod);

            //Update the RecyclerView
            notifyItemInserted(oldListSize - getItemCount() - 1);
        

        @Override
        public void onChildChanged(DataSnapshot dataSnapshot, String s) 

        

        @Override
        public void onChildRemoved(DataSnapshot dataSnapshot) 
            String apodKey = dataSnapshot.getKey();
            int apodIndex = apodListIds.indexOf(apodKey);

            if (apodIndex > -1) 

                // Remove data and IDs from the list
                apodListIds.remove(apodIndex);
                apodList.remove(apodIndex);

                // Update the RecyclerView
                notifyDataSetChanged();
            
        

        @Override
        public void onChildMoved(DataSnapshot dataSnapshot, String s) 

        

        @Override
        public void onCancelled(DatabaseError databaseError) 

        

    ;
    ref.addChildEventListener(childEventListener);
    mChildEventListener = childEventListener;

 

   @Override
    public int getItemCount() 
    return apodList.size();

    public void cleanupListener() 
    if (mChildEventListener != null) 
        mDatabaseReference.removeEventListener(mChildEventListener);
    
  


【问题讨论】:

我认为问题在于配置更改后的新 RV 在 onCreate 结束时对其视图内容一无所知,因为该数据仅在所有子侦听器触发后才会出现。所以它不知道如何完全恢复自己,因为它认为它当时是空的。我不知道临时解决方案,但确实想提供该信息。如果有帮助,您可以查询 RV 的滚动偏移量。 @DougStevenson。非常感谢 Doug,我尝试保存和恢复该位置,但无法使其正常工作。我认为这篇文章会引起更多关注,因为我要么完全遗漏了某些东西,要么 Firebase 很难实现这种东西。我会继续尝试,如果我发现任何其他内容,我会更新我的帖子。 【参考方案1】:

虽然旋转活动再次被销毁并重新创建,这意味着您的所有对象都被销毁并重新创建,并且布局也重新绘制。

如果您想停止重新创建活动,您可以使用 android:configChanges,但这是不好的做法,也不是正确的方法。

现在唯一剩下的就是在旋转之前保存recyclerview的位置,并在重新创建活动后获取它。

//保存recyclerview位置

 @Override
    public void onSaveInstanceState(Bundle savedInstanceState) 
      // Save UI state changes to the savedInstanceState.
      // This bundle will be passed to onCreate if the process is
      // killed and restarted.
      savedInstanceState.putInt("position", mRecyclerView.getAdapterPosition()); // get current recycle view position here.
      super.onSaveInstanceState(savedInstanceState);
    

//恢复值

    @Override
protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);

    PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
    setContentView(R.layout.activity_main);
    getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));

    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    final int columns = getResources().getInteger(R.integer.gallery_columns);

    mDatabaseReference = FirebaseDatabase.getInstance().getReference();
    query = mDatabaseReference.child("apod").orderByChild("date");

    mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
    mRecyclerView.setHasFixedSize(true);
    mRecyclerView.setLayoutManager(new GridLayoutManager(this, columns));


    mRecyclerAdapter = new RecyclerAdapter(this, query);
    mRecyclerView.setAdapter(mRecyclerAdapter);
   if(savedInstanceState != null)
     // scroll to existing position which exist before rotation.
     mRecyclerView.scrollToPosition(savedInstanceState.getInt("position"));
     

  

希望这些对你有所帮助。

【讨论】:

谢谢@jitesh,我以前试过这个,但还不够。一个原因是我必须添加Handler。当我终于能够让它工作时,它仍然有一个可怕的延迟,它刷新了整个屏幕,我猜是因为notifyItemInserted(oldListSize - getItemCount() - 1); 在新的Activity 上再次被调用。解决这个问题的唯一方法是使用android:configChanges,我知道它不受欢迎,但在方向变化期间它看起来不那么丑陋了。 没有方法RecyclerView.getAdapterPosition() 没有方法getAdapterPosition() getAdapterPosition 不可用,但您可以从布局管理器中获取,例如LinearLayoutManager.findFirstVisibleItemPosition() 你也需要这个:【参考方案2】:

编辑:

我终于能够使用多种因素完成这项工作。如果其中任何一个被遗漏,它根本就行不通。

在我的 onCreate 中创建新的GridLayoutManager

gridLayoutManager = new GridLayoutManager(getApplicationContext(), columns);

在 onPause 中保存 RecyclerView 状态

private final String KEY_RECYCLER_STATE = "recycler_state"; 
private static Bundle mBundleRecyclerViewState;
private Parcelable mListState = null;

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

    mBundleRecyclerViewState = new Bundle();
    mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();
    mBundleRecyclerViewState.putParcelable(KEY_RECYCLER_STATE, mListState);

AndroidManifest.xml 中包含configChanges

保存和恢复RecyclerView 状态是不够的。我还不得不逆向,将我的 AndroidManifest.xml 更改为 'android:configChanges="orientation|screenSize"'

<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize">

使用Handler 恢复onConfigurationChanged 中的RecyclerView 状态

处理程序是最重要的部分之一,如果不包括在内,它将根本无法工作,RecyclerView 的位置将重置为 0。我想这是几年前的一个缺陷,并且从未修复。然后我根据方向更改了GridLayoutManagersetSpanCount

@Override
public void onConfigurationChanged(Configuration newConfig) 
    super.onConfigurationChanged(newConfig);
    columns = getResources().getInteger(R.integer.gallery_columns);

    if (mBundleRecyclerViewState != null) 
        new Handler().postDelayed(new Runnable() 
            @Override
            public void run() 
                mListState = mBundleRecyclerViewState.getParcelable(KEY_RECYCLER_STATE);
                mRecyclerView.getLayoutManager().onRestoreInstanceState(mListState);
            
        , 50);
    

    // Checks the orientation of the screen
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 
        gridLayoutManager.setSpanCount(columns);
     else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) 
        gridLayoutManager.setSpanCount(columns);
    
    mRecyclerView.setLayoutManager(gridLayoutManager);

就像我说的,所有这些更改都需要包括在内,至少对我来说是这样。我不知道这是否是最有效的方法,但花了很多时间后,它对我有用。

【讨论】:

【参考方案3】:

每次方向发生变化时都会重新创建活动。因此,您必须将位置保存在捆绑包中,然后在第二次调用 onCreate() 时重新使用它。

@Override
public void onCreate(Bundle savedInstanceState) 

   super.onCreate(savedInstanceState);

   int recyclerViewPosition;
   if(savedInstanceState != null)
     recyclerViewPosition = savedInstanceState.getInt(RECYCLER_VIEW_POSITION);
   

   mRecyclerView.scrollToPosition(recyclerViewPosition);



@Override
public void onSaveInstanceState(Bundle outState) 
    super.onSaveInstanceState(outState);

    //save the current recyclerview position 
    outState.putInt(RECYCLER_VIEW_POSITION, mRecyclerView.getAdapterPosition());

您可能想查看 RecyclerView.State() link,了解有关如何恢复回收站视图状态的更多选项。

【讨论】:

【参考方案4】:

实际上有更好的方法可以在 Activity 重新创建时恢复回收站视图位置,这是通过使用回收站视图布局管理器选项来存储和恢复最后一个位置(最后一个回收站视图布局状态)。看看这个tutorial。

简单来说,需要在对应的activity方法里面调用布局管理器方法onSaveInstanceState()和onRestoreInstanceState(Parcelable state)。最后,您必须在使用数据填充适配器后立即恢复布局管理器状态。

如果你看一下 LinearLayoutManager 的实现,你会看到这个类是如何管理状态(存储/恢复)的

【讨论】:

【参考方案5】:

我终于做到了。无需修改或更改 manifest.xml。 看了好久终于找到重点了:

因此,如果您在每次屏幕旋转时异步加载数据 然后通过 Looper 队列将其发布到主线程,你 总是会丢失之前的滚动位置。实际上, 任何

handler.post(this::setListItems);

也会破坏它,因为这个动作将被发布到 在列表已经开始布局后排队和交付。

因此,如果要恢复回收站视图状态,则必须使用 Handler 或缓存数据。这是代码示例(androidx 片段)。

private static String STATE = "STATE";
    
    // the recycler view manager
    private GridLayoutManager layoutManager;
    
    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) 
        super.onSaveInstanceState(outState);
        // save recycler view layout
        if(layoutManager != null) 
            outState.putParcelable(STATE, layoutManager.onSaveInstanceState());
        
    
    
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) 
                             
        ...
        

        loadDataAsync(savedInstanceState);
        
    
        return view;
    
    
    private void loadDataAsync(Bundle savedInstanceState)
    
        repository.getAll(new IMyListener())
                @Override
                public void getAll(List<String> listResult) 
                        reyclerViewFilterableAdapter.setItems(listResult);
                        reyclerViewFilterableAdapter.notifyDataSetChanged();
                        new Handler().postDelayed(new Runnable() 

                            @Override
                            public void run() 
                                if (layoutManager != null) 
                                    layoutManager.onRestoreInstanceState(savedInstanceState.getParcelable(STATE));
                                
                            
                        , 100);
                
            );
    
    

来源:https://medium.com/@dimezis/android-scroll-position-restoring-done-right-cff1e2104ac7

【讨论】:

以上是关于在使用 Firebase 和 ChildEventListener 时,如何在方向更改后保留 RecyclerView 的位置?的主要内容,如果未能解决你的问题,请参考以下文章

$emit 不会触发子事件

使用选项配置 firebase 在 Xcode 11 和 Firebase 6.19 中崩溃

如何在项目中使用 firebase 和 firebase 功能的现有 Flutter 应用程序中更新包名称?

使用 Firebase 和 cordova-plugin-firebase 在 Apache Cordova Android 应用程序上构建问题

在 Flutter Web 和移动应用中使用 Firebase

在中国使用 Firebase 身份验证和 Firestore