Видео-сообщение как в Telegram. Часть вторая — Оптимизация

от автора

Часто ли вы пользуетесь Telegram?
Если да, то скорее всего вы хотя бы раз отправляли «кружочки». В этой серии статьей мы напишем небольшой проект с отображением списка видео-сообщений.
Для отображения будем использовать ExoPlayer, настроим сохранение видео в кеш, а также напишем свой TimeBar для управления видео.

Оглавление

Введение

Вначале каждой части я прикреплю git-ветку, в которой будут изменения, описанные в статье. Вы можете либо сразу скачать ее и запустить проект, либо самостоятельно пошагово писать код.
Также можете взять git-ветку предыдущей части и сразу понять контекст.

Git-ветка этой части.

Оптимизация

В этой части мы будем решать проблемы, с которыми столкнулись в предыдущей статье:

  • Видео не успевают воспроизвестись при прокрутке элементов

  • При быстрой прокрутке проседает fps

Оптимизация будет состоять из трех этапов:

  • Асинхронный inflate вью плеера

  • Отложенная загрузка видео

  • Отображение миниатюры первого кадра видео

Асинхронный inflate вью плеера

После профилирования приложения становится понятно, что плавность прокрутки ухудшается из-за тяжелого инфлейта PlayerView.
Сделаем создание вью асинхронным с помощью AsyncLayoutInflater.

Подключим его к проекту.

app/build.gradle.kts

...  dependencies {     ...     implementation("androidx.asynclayoutinflater:asynclayoutinflater:1.0.0") }

Добавим сущность ViewHolderPool, который будет хранить в себе созданные вью плеера.

ViewHolderPool.kt

private const val CACHE_SIZE = 7  class ViewHolderPool(recyclerView: RecyclerView) {          private var inflater = AsyncLayoutInflater(recyclerView.context)     private var viewCache = ArrayDeque<View>()      // при инициализации асинхронно создаем 7 view.     // кол-во кеша можно настраивать в зависимости от того     // сколько view помещается на экран     init {         repeat(CACHE_SIZE) {             inflater.inflate(R.layout.li_bubble, recyclerView) { view, _, _ ->                 viewCache.add(view)             }         }     }      fun getOrNull(): View? {         return viewCache.removeFirstOrNull()     } }

Добавим ViewHolderPool в BubbleAdapter.

BubbleAdapter.kt

class BubbleAdapter(     private val items: List<BubbleModel> ) : RecyclerView.Adapter<BubbleViewHolder>() {      private var viewHolderPool: ViewHolderPool? = null      // при присоединении adapter'а к recycler'у инициализируем ViewHolderPool     override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {         super.onAttachedToRecyclerView(recyclerView)         viewHolderPool = viewHolderPool ?: ViewHolderPool(recyclerView)     }      // при отсоединении adapter'а от recycler'у зануляем ViewHolderPool     override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {         super.onDetachedFromRecyclerView(recyclerView)         viewHolderPool = null     }      // при создании viewHolder'а либо используем либо созданную асинхронно view,     // либо создаем новую в методе inflateNewView()     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BubbleViewHolder {         val cachedView = viewHolderPool?.getOrNull()         val inflatedView = cachedView ?: inflateNewView(parent)         return BubbleViewHolder(inflatedView)     }      private fun inflateNewView(parentView: ViewGroup): View {         val inflater = LayoutInflater.from(parentView.context)         return inflater.inflate(R.layout.li_bubble, parentView, false)     }     ... }

Запускаем и видим что скролл стал более плавным, но еще далек от идеала.
При fling-прокрутке плавность все же проседает и видео не успевает подгружаться.

Отложенная загрузка контента

Профилируем еще и видим, что теперь плавность прокрутки ухудшается из-за загрузки видео внутри player.setMediaSource(mediaSource).

Чтобы минимизировать кол-во загрузок поставим debounce в 700мс внутри метода bind().

BubbleViewHolder.kt

private const val LOAD_VIDEO_DEBOUNCE_MS = 700L  class BubbleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), CoroutineScope {      private var loadVideoJob: Job? = null      ...      // подробно на теме корутин останавливаться не буду, тк не по теме статьи     override val coroutineContext: CoroutineContext         get() = Job() + Dispatchers.Main      fun bind(model: BubbleModel) {         // отменяем предыдущую загрузку предыдущего видео         loadVideoJob?.cancel()         loadVideoJob = launch {             delay(LOAD_VIDEO_DEBOUNCE_MS)             val mediaSource = mediaSourceFactory.createMediaSource(model.videoUrl)             // начинаем загрузку нового видео после 700мс задержки             player.setMediaSource(mediaSource)             player.prepare()             player.play()         }     } }

Запускаем и видим что скролл стал плавным. Но появилась другая проблема — после прокрутки видео «дергается». Происходит это из-за того что новое видео начинает загружаться с задержкой, и на мгновение мы видим старое видео с предыдущего вызова метода bind().

Чтобы это починить будем отображать первый кадр нового видео в ImageView без задержек. И скроем ImageView, как только новое видео начнет воспроизводиться.

Таким образом пока видео будет загружаться его перекроет ImageView и старого видео мы не увидим.

Отображение миниатюры первого кадра видео

Для большинства форматов видео есть возможность отображения первого кадра с помощью обычных библиотек загрузки картинок (Glide, Picasso итд).

Подключим Glide к проекту.

app/build.gradle.kts

dependencies {     ...     implementation("com.github.bumptech.glide:glide:4.16.0") }

Добавим ImageView для отображения превью видео.

li_bubble.xml

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout     xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="wrap_content"     android:layout_height="wrap_content"     android:layout_margin="50dp">      ...      <!-- убедись что AppCompatImageView перекрывает PlayerView -->     <androidx.appcompat.widget.AppCompatImageView       android:id="@+id/li_bubble_thumbnail"       android:layout_width="0dp"       android:layout_height="0dp"       android:scaleType="centerCrop"       app:layout_constraintBottom_toBottomOf="@+id/li_bubble_player_view"       app:layout_constraintEnd_toEndOf="@+id/li_bubble_player_view"       app:layout_constraintStart_toStartOf="@+id/li_bubble_player_view"       app:layout_constraintTop_toTopOf="@+id/li_bubble_player_view" />  </androidx.constraintlayout.widget.ConstraintLayout>

Чтобы отследить начало воспроизведения видео нам нужна реализация Player.Listener.

BubblePlayerListener.kt

class BubblePlayerListener(     private val viewHolder: BubbleViewHolder ) : Player.Listener {          override fun onIsPlayingChanged(isPlaying: Boolean) {         if (isPlaying) {             viewHolder.onFinishLoadVideo()         }     } }

В BubbleViewHolder добавим методы, которые будут показывать и скрывать наше превью.

BubbleViewHolder.kt

class BubbleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), CoroutineScope {      ...      private val listener = BubblePlayerListener(this)     private val thumbnail = itemView.findViewById<ImageView>(R.id.li_bubble_thumbnail)     private val player = ExoPlayer.Builder(itemView.context).build().apply {         repeatMode = ExoPlayer.REPEAT_MODE_ONE         addListener(listener)     }      ...      fun bind(model: BubbleModel) {         onStartLoadVideo(model.videoUrl)         ...     }          fun onFinishLoadVideo() {         thumbnail.isVisible = false     }          private fun onStartLoadVideo(videoUrl: String) {         thumbnail.isVisible = true         Glide.with(thumbnail)             .load(videoUrl)             // загружаем первый кадр             .apply(RequestOptions().frame(0))             .into(thumbnail)     } }

На этом работа по оптимизации закончена. Давайте добавим скругление углов в нашем сообщении и посмотрим на результат.

BubbleViewHolder.kt

class BubbleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), CoroutineScope {      ...      init {         ...         itemView.outlineProvider = object : ViewOutlineProvider() {             override fun getOutline(view: View, outline: Outline) {                 outline.setOval(0, 0, view.width, view.height)             }         }         itemView.clipToOutline = true     }     ... }

Заключение

Все готово! Запускаем и видим достаточно плавное отображение наших видео-сообщений.
В следующей статье мы обработаем раскрытие видео по нажатию и добавление контроллов для управления видео.

Читать далее: Часть 3. Контролы и раскрытое состояние


ссылка на оригинал статьи https://habr.com/ru/articles/776294/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *