【问题标题】:RecyclerView inside NestedScrollview alternativeNestedScrollview 替代方案中的 RecyclerView
【发布时间】:2020-09-30 10:43:03
【问题描述】:

我有一个具有这种层次结构的布局







线性布局>

有时我需要更新我的 recyclerview 元素,但它会冻结主线程。 我猜是因为滚动视图需要再次测量它。 我真的很想知道我应该怎么做?

  • 用 layoutinlfate 替换回收站视图?
  • 高度固定的回收站视图?
  • 用recyclview 替换nestedscrollview?将在reclyerview 中有recyclview。这行得通吗?

【问题讨论】:

  • 您应该在问题中发布布局 xml 和其他相关代码块。
  • 制作demo作品有问题吗?
  • 它总是让我感到困惑,有理由将回收视图放入滚动视图。 Recyclerview 本身是可滚动的,嵌套滚动元素总是我尽量避免的事情(即使这意味着与 UX 设计师争论)。 ..but it freeze main thread 我认为您的问题是由于主线程上的错误 recyclerview 更新引起的。布局应该可以工作,如果 recylerview 项目更新,Scrollview 将不会自行布局!仅当 Recyclerview 的高度发生变化时
  • 这就是问题所在。 Recyclerview 高度将会改变
  • 你到底想达到什么目的?

标签: android android-recyclerview


【解决方案1】:

这是一种常见的 UI 模式,android:nestedScrollingEnabled="true" 无法解决此问题。

此模式的最佳方法是使用具有多种视图类型的单个回收器视图,而不是使用嵌套元素。结果是一个更复杂的 recyclerview,但你有更好的性能和更多的控制。

【讨论】:

  • 是的,如果 recyclerview 有很多项目,这是要走的路
  • 此外,您还可以使用 concatAdapter 因为您的项目保持顺序medium.com/androiddevelopers/…
【解决方案2】:

尝试用如下标签包裹当前布局:

< RelativeLayout> // math_parent
< NestedScrollView fillViewPort=true>
< LinearLayout>
< Viewgroup/>
< ViewGroup/>
< RecyclerView/>
< ViewGroup/>
< /LinearLayout>
< /NestedScrollView>
< /RelativeLayout>

它会再次阻止滚动视图和recyclerview措施。

【讨论】:

    【解决方案3】:

    首先,基于Android文档:

    永远不要将 RecyclerView 或 ListView 添加到滚动视图。这样做 导致糟糕的用户界面性能和糟糕的用户体验。

    更新 UI 总是在主线程上完成。因此,当您要更新 RecyclerView 时,它会影响性能。为了更好的用户体验,您可以在另一个线程中进行计算并在主线程中更新 UI。在下面的代码中,我模拟了你的情况:

    activity_main.xml

    <androidx.constraintlayout.widget.ConstraintLayout 
    
        <androidx.core.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent">
    
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
    
            <ImageView
                android:layout_width="match_parent"
                android:layout_height="700dp"
                android:background="@color/black"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:nestedScrollingEnabled="false"/>
    
            </LinearLayout>
    
        </androidx.core.widget.NestedScrollView>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    在我的示例中,更新列表每秒钟发生一次。这是代码:

     public class MainActivity extends AppCompatActivity {
        
            private MyAdapter myAdapter;
            int cnt = 0;
        
            @Override
            protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_main);
        
                myAdapter = new MyAdapter();
        
                RecyclerView recyclerView = findViewById(R.id.recycler_view);
                LinearLayoutManager layoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
                recyclerView.setLayoutManager(layoutManager);
                recyclerView.setAdapter(myAdapter);
        
        
                AsyncTask.execute(() -> updateList());
        
        
            }
        
            private void updateList() {
        
                Timer timer = new Timer();
                timer.schedule(new TimerTask() {
        
                    @Override
                    public void run() {
                        cnt++;
                        myAdapter.addToList(cnt + "");
        
                    }
        
                }, 1, 1000);//Update List every second
            }
        }
    

    列表通过 updateList() 方法更新。为了在新线程中运行它,我使用 AsyncTask :

    AsyncTask.execute(() -> updateList());
    

    最后是我的适配器:

    public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
       
        List<String> list = new ArrayList<>(); 
    
        @NonNull
        @Override
        public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
            return new MyViewHolder(itemView);
        }
    
        @Override
        public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
            holder.textView.setText(list.get(position));
        }
    
        @Override
        public int getItemCount() {
            return list.size();
        }
    
        public class MyViewHolder extends RecyclerView.ViewHolder {
    
            TextView textView;
    
            public MyViewHolder(@NonNull View itemView) {
                super(itemView);
                textView = itemView.findViewById(R.id.text_view);
    
            }
        }
    
        public void addToList(String s) {
            list.add(s);
            new Handler(Looper.getMainLooper()).post(() -> {
                notifyItemInserted(list.size());
            });
        }
    }
    

    可以看出notifyItemInserted 在 Handler 中被调用来更新主线程上的 UI。不同情况下通知回收商可以参考https://stackoverflow.com/a/48959184/12500284

    【讨论】:

      【解决方案4】:

      您可以为nestedscrollview 中的所有视图创建多个适配器,然后使用concat 适配器连接它们。就我而言,我的nestedscrollview 有一个标题和一个recyclerview。所以我为标题创建了一个适配器,为回收站视图创建了一个。您可以使用 lambda 或接口在标头适配器和活动之间进行通信。代码如下:

      val listAdapter = ManageQuoteAdapter(this)
      val headerAdapter = ManageQuoteHeaderAdapter(handler)
      val concatAdapter = ConcatAdapter(headerAdapter, listAdapter)
      recyclerView.layoutManager = LinearLayoutManager(this)
      recyclerView.adapter = concatAdapter
      

      优点:

      1. 更好的性能,特别是如果您的 recyclerview 有很多图像。之前所有的图片都是一次性加载的,最终会抛出内存异常
      2. 最适合具有页眉、页脚和项目列表的简单视图。

      缺点:

      1. 您至少需要再创建一个适配器文件。所以需要维护更多文件。
      2. 如果您在页眉或页脚部分(示例 - 表单)中有如此多的操作、输入字段等,复杂性将会增加。在这种情况下,NestedScrollView 会更合适,因为无论如何 recyclerview 都会有更少的项目。

      我的屏幕如下所示:

      【讨论】:

      • 我认为这个答案对于这个(常见)问题很有希望,但你的答案似乎自相矛盾。您说“您可以为 nestedscrollview 中的所有视图创建多个适配器,然后使用 concat 适配器连接它们。”然后在 RecyclerView 上演示代码设置适配器。是哪个?
      【解决方案5】:

      UI 冻结的最可能原因是您正在从某个速度较慢的来源(例如网络或数据库)检索数据。

      如果是这种情况,解决方案是在后台线程上检索数据。这些天来,我建议使用 Kotlin Coroutines 和 Kotlin Flow。

      你的DB会返回一个Flow,每当DB中的数据发生变化时,它的内容就会自动更新,你可以在ViewModel中观察数据的更新。

      从您的数据库中获取一个 Flow 对象:

      @Dao
      interface MyDataDao {
      
          @Query("SELECT * FROM mydata")
          fun flowAll(): Flow<List<MyDataEntity>>
      }
      

      Fragment 会观察 LiveData 并在列表适配器发生变化时更新它:

      class MyScreenFragment(): Fragment() {
      
          override fun onCreate() {
              viewModel.myDataListItems.observe(viewLifecycleOwner) { listItems ->
                  myListAdapter.setItems(listItems)
                  myListAdapter.notifyDataSetChanged()
              }
          }
      }
      

      在 ViewModel 中:

      @ExperimentalCoroutinesApi
      class MyScreenViewModel(
          val myApi: MyApi,
          val myDataDao: MyDataDao
      ) : ViewModel() {
      
          val myDataListItems = MutableLiveData<List<MyDataListItem>>()
      
          init {
              // launch a coroutine in the background to retrieve our data 
              viewModelScope.launch(context = Dispatchers.IO) { 
                  // trigger the loading of data from the network
                  // which you should then save into the database,
                  // and that will trigger the Flow to update
                  myApi.fetchMyDataAndSaveToDB()
                  collectMyData()
              }
          }
      
          /** begin collecting the flow of data */
          private fun collectMyData() {
              flowMyData.collect { myData ->
                  // update the UI by posting the list items to a LiveData
                  myDataListItems.postValue(myData.asListItems())
              }
          }
      
          /** retrieve a flow of the data from database */
          private fun flowMyData(): Flow<List<MyData>> {
              val roomItems = myDataDao.flowAll()
              return roomItems.map { it.map(MyDataEntity::toDomain) }
          }
      
          /** convert the data into list items */
          private fun MyData.asListItems(): MyDataListItem {
              return this.map { MyDataListItem(it) }
          }
      }
      

      如果您不知道如何在房间数据库中定义对象,我会给您以下提示:

      // your data representation in the DB, this is a Room entity
      @Entity(tableName = "mydata")
      class MyDataEntity(
          @PrimaryKey
          val id: Int,
          val date: String,
          // ...
      )
      
      // Domain representation of your data
      data class MyData(
          val id: Int,
          val date: SomeDateType,
          // ...
      )
      
      // Map your DB entity to your Domain representation
      fun MyDataEntity.toDomain(): MyData {
          return MyData(
              id = id,
              date = SomeDateFormatter.format(date),
              // ...
          )
      }
      

      【讨论】:

        【解决方案6】:

        当您在垂直 NestedScrollView 中使用垂直回收器视图而不使用恒定高度时,即使您将 RecyclerView 高度设置为 match_parent,RecyclerView 的高度也会为 wrap_content。因此,当您更新 RecyclerView 数据时,MainThread 将被冻结,因为将调用所有 RecyclerView 项目的 onBind() 方法。这意味着 RecyclerView 在滚动期间不会回收任何东西!如果您不能在设计中使用 MultipleViewType,请尝试为 RecyclerView 设置一个恒定的高度。 这个解决方案给你带来了一些困难。比如处理可滚动性。

        【讨论】:

          猜你喜欢
          • 2018-11-29
          • 2016-02-04
          • 2015-01-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2017-02-15
          • 2017-05-28
          相关资源
          最近更新 更多