RecyclerView: вызов notifyDataSetChanged во время фрагмента перехода == замораживание

#android #android-fragments #android-recyclerview #android-animation #android-transitions

#Android #android-фрагменты #android-recyclerview #android-анимация #android-переходы

Вопрос:

Я не нашел подобных вопросов, поэтому спрашиваю. У меня есть: первый фрагмент, второй фрагмент и переход:

 <navigation xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:tools="http://schemas.android.com/tools"
            android:id="@ id/nav_graph"
            app:startDestination="@id/FirstFragment">

    <fragment
        android:id="@ id/FirstFragment"
        android:name="testrenderinglistontransition.FirstFragment"
        android:label="@string/first_fragment_label"
        tools:layout="@layout/fragment_first">

        <action
            app:enterAnim="@anim/right_in"
            android:id="@ id/action_FirstFragment_to_SecondFragment"
            app:destination="@id/SecondFragment"/>
    </fragment>
    <fragment
        android:id="@ id/SecondFragment"
        android:name="testrenderinglistontransition.SecondFragment"
        android:label="@string/second_fragment_label"
        tools:layout="@layout/fragment_second">

        <action
            app:enterAnim="@anim/right_in"
            android:id="@ id/action_SecondFragment_to_FirstFragment"
            app:destination="@id/FirstFragment"/>
    </fragment>
</navigation>
 

Во втором фрагменте я эмулирую получение данных и показываю данные во время перехода:

класс SecondFragment : Фрагмент() {

 override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    // Inflate the layout for this fragment
    return inflater.inflate(R.layout.fragment_second, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    val recyclerview : RecyclerView = view.findViewById(R.id.recyclerview)
    val progress : ProgressBar = view.findViewById(R.id.progress)
    val adapter = CustomAdapter(arrayOf())
    progress.visibility =View.VISIBLE
    recyclerview.adapter = adapter
    recyclerview.layoutManager = LinearLayoutManager(context).apply {
        orientation = LinearLayoutManager.VERTICAL
    }

    val data = getData()

    Handler().postDelayed(Runnable {
        progress.visibility =View.GONE
        adapter.dataSet = data
        adapter.notifyDataSetChanged()
    },250) // transition time is 300 so we should call notifyDataSetChanged before ending of transition

}

private fun getData() : Array<String>{
    val list = mutableListOf<String>()
    for(i in 0 .. 1000){
        list.add(UUID.randomUUID().toString())
    }
    return list.toTypedArray()
}
 

}

Что у меня есть:

У вас есть какие-либо идеи, как это исправить? Я думаю, мне следует дождаться окончания перехода.

Вы можете взглянуть на адаптер и макет, но они действительно самые простые:

 class CustomAdapter(var dataSet: Array<String>) :
    RecyclerView.Adapter<CustomAdapter.ViewHolder>() {

    /**
     * Provide a reference to the type of views that you are using
     * (custom ViewHolder).
     */
    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val textView: TextView

        init {
            // Define click listener for the ViewHolder's View.
            textView = view.findViewById(R.id.textView)
        }
    }

    // Create new views (invoked by the layout manager)
    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        // Create a new view, which defines the UI of the list item
        val view = LayoutInflater.from(viewGroup.context)
            .inflate(R.layout.text_row_item, viewGroup, false)

        return ViewHolder(view)
    }

    // Replace the contents of a view (invoked by the layout manager)
    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {

        // Get element from your dataset at this position and replace the
        // contents of the view with that element
        viewHolder.textView.text = dataSet[position]
    }

    // Return the size of your dataset (invoked by the layout manager)
    override fun getItemCount() = dataSet.size

}

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="30dp"
    android:layout_marginLeft="4dp"
    android:layout_marginRight="4dp"
    android:gravity="center_vertical">

    <ImageView
        android:id="@ id/image"
        android:src="@android:drawable/alert_dark_frame"
        android:layout_width="30dp"
        android:layout_height="30dp"/>

    <TextView
        android:layout_alignLeft="@ id/image"
        android:id="@ id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>
 

Ответ №1:

Если вы хотите дождаться завершения анимации, вы должны переопределить onCreateAnimator#onAnimationEnd свой Fragment , а затем вызвать метод для «инициализации» ваших представлений следующим образом:

 override fun onCreateAnimator(transit: Int, enter: Boolean, nextAnim: Int): Animator? {
    var animator = super.onCreateAnimator(transit, enter, nextAnim)
    
    if (enter) {
        animator?.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                super.onAnimationEnd(animation)
                initView()
            }
        })
    }

    return animator        
}

private fun initView() {
    // TODO create your recycler views etc here
}
 

Однако эта строка:

 val data = getData() 
 

должен вызываться из отдельного потока, поскольку это действительно то, что вызывает зависание пользовательского интерфейса (вы выполняете много работы над потоком пользовательского интерфейса), что-то вроде:

 fun loadData() = CoroutineScope(Dispatchers.Main).launch {
        // TODO show progress bar here
        val task = async(Dispatchers.IO) {
            getData()
        }
        data = task.await()
        adapter.dataSet = data
        adapter.notifyDataSetChanged()
        // TODO hide progress bar here
    }
 

loadData() теперь можно вызывать вместо всех этих строк:

  val data = getData()

    Handler().postDelayed(Runnable {
        progress.visibility =View.GONE
        adapter.dataSet = data
        adapter.notifyDataSetChanged()
    },250) // transition time is 300 so we should call notifyDataSetChanged before ending of transition
 

Комментарии:

1. В моем примере getData() просто создает массив, и это действительно быстро, в реальном случае мы должны использовать асинхронный код, вы правы

2. Это не очень быстро, поэтому вы можете видеть паузу в своем пользовательском интерфейсе. Несмотря на то, что это всего лишь пара миллисекунд, это все равно вызывает замораживание пользовательского интерфейса

3. Я протестировал это — асинхронная загрузка данных (или мы получаем данные из статического поля, подготовленного ранее, или что-то еще) не решает проблему. Но в любом случае большое спасибо за ваш совет.

Ответ №2:

Ответ Дэвида Кроукампа мне не помогает, потому что super.onCreateAnimator(transit, enter, nextAnim) возвращает значение null.

Но у меня есть другое решение:

 Handler().postDelayed(Runnable {
        progress.visibility =View.GONE
        adapter.dataSet = data
        adapter.notifyDataSetChanged()
    },300) // transition time is 300 so we will wait end of transition.
 

Есть один недостаток:
Если загрузка данных занимает 200 мс, мы получим данные через 200 300 мс, поэтому 200 мс мы ждем без какой-либо причины. Но мы можем измерить время загрузки данных.