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

от автора

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

Оглавление

Введение

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

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

Настройка проекта

Так как кружок будет элементом чата, стоит позаботиться о его корректном отображении внутри RecyclerView. Добавим его в верстку нашего экрана.

activity_main.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"     xmlns:tools="http://schemas.android.com/tools"     android:layout_width="match_parent"     android:layout_height="match_parent"     tools:context=".MainActivity">      <androidx.recyclerview.widget.RecyclerView         android:id="@+id/bubbles_rv"         android:layout_width="match_parent"         android:layout_height="wrap_content"         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"         app:layout_constraintEnd_toEndOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent" />  </androidx.constraintlayout.widget.ConstraintLayout>

Создаем data-модель нашего видео сообщения с ссылкой на видео.

BubbleModel.kt

class BubbleModel(val videoUrl: String)

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

app/build.gradle.kts

...  dependencies {     ...     implementation("com.google.android.exoplayer:exoplayer:2.16.1")     implementation("com.google.android.exoplayer:extension-okhttp:2.16.1") }

Создаем верстку элемента списка

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">      <com.google.android.exoplayer2.ui.PlayerView         android:id="@+id/li_bubble_player_view"         android:layout_width="220dp"         android:layout_height="220dp"         app:resize_mode="zoom"         app:surface_type="texture_view"         app:use_controller="false"         app:shutter_background_color="@android:color/transparent"         app:layout_constraintBottom_toBottomOf="parent"         app:layout_constraintEnd_toEndOf="parent"         app:layout_constraintStart_toStartOf="parent"         app:layout_constraintTop_toTopOf="parent" />  </androidx.constraintlayout.widget.ConstraintLayout>
  • app:resize_mode=»zoom» — то как видео будет подстраиваться под размеры плеера.

  • app:surface_type=»texture_view» — тип view на котором будет отрисовываться наше вью. Почему-то при дефолтном surface_view ячейки recyclerView накладываются друг на друга. Поэтому используем texture_view.

  • app:use_controller=»false» — скрываем контроллы видео (кнопка плей, паузы, перемотки итд).

  • app:shutter_background_color=»@android:color/transparent» — задаем прозрачный цвет у фона плеера.

Для отображения элементов списка нам понадобится реализация RecyclerView.ViewHolder.

BubbleViewHolder.kt

class BubbleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {      private val playerView = itemView.findViewById<PlayerView>(R.id.li_bubble_player_view)      fun bind(model: BubbleModel) {       // реализация будет ниже     } }

Для управления viewHolder’ами добавим реализацию RecyclerView.Adapter.

BubbleAdapter.kt

class BubbleAdapter(     private val items: List<BubbleModel> ) : RecyclerView.Adapter<BubbleViewHolder>() {      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BubbleViewHolder {         val inflater = LayoutInflater.from(parent.context)         val view = inflater.inflate(R.layout.li_bubble, parent, false)         return BubbleViewHolder(view)     }      override fun getItemCount() = items.size      override fun onBindViewHolder(holder: BubbleViewHolder, position: Int) {         holder.bind(items[position])     } }

Подключим adapter к recyclerView и добавим 30 видео-сообщений.

MainActivity.kt

class MainActivity : AppCompatActivity() {      private val items = mutableListOf<BubbleModel>().apply {         repeat(30) {             add(BubbleModel("https://i.imgur.com/3Y8IRmz.mp4"))         }     }      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContentView(R.layout.activity_main)          val recyclerView = findViewById<RecyclerView>(R.id.bubbles_rv)         recyclerView.adapter = BubbleAdapter(items)     } }

Для загрузки видео нам потребуются права на доступ в сеть.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?> <manifest>      <uses-permission android:name="android.permission.INTERNET"/>      <application       ...     </application> </manifest>

Добавим кеш для отображения загруженных видео.

VideoCache.kt

private const val DOWNLOAD_CONTENT_DIRECTORY = "inner_video_cache" private const val MAX_CACHE_SIZE_IN_BYTES = 100 * 1024 * 1024  object VideoCache {      private var cache: SimpleCache? = null      fun getInstance(context: Context): SimpleCache {         return cache ?: run {             //путь до файла в котором будет храниться кеш видео             val cacheDir = File(context.externalCacheDir, DOWNLOAD_CONTENT_DIRECTORY)             //стратегия очистки кеша (очистка последнего использованного кеша)             val evictor = LeastRecentlyUsedCacheEvictor(MAX_CACHE_SIZE_IN_BYTES.toLong())             val databaseProvider = StandaloneDatabaseProvider(context)             SimpleCache(cacheDir, evictor, databaseProvider).apply {                 cache = this             }         }     } }

За загрузку видео внутри библиотеки ExoPlayer отвечает MediaSource. Добавим фабрику для этой сущности.

MediaSourceCreator.kt

class MediaSourceFactory(     private val context: Context ) {      private val mediaSourceFactory by lazy {         val cacheSink = CacheDataSink.Factory().setCache(VideoCache.getInstance(context))         val upstreamFactory = DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory())         val cacheDataSourceFactory = CacheDataSource.Factory()             //то куда будет сохраняться наш кеш. Если не указывать, то кеш будет read-only             .setCacheWriteDataSinkFactory(cacheSink)             //собственно сам кеш             .setCache(VideoCache.getInstance(context))             //то откуда будет подргужаться наше видео             .setUpstreamDataSourceFactory(upstreamFactory)             //игнорируем ошибки при зависи в кеш             .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)         //Нужно выбрать MediaSource в соответсвии с форматом видео. Для большинства форматов подходит ProgressiveMediaSource         ProgressiveMediaSource.Factory(cacheDataSourceFactory)     }      fun createMediaSource(url: String): MediaSource {         return mediaSourceFactory.createMediaSource(MediaItem.fromUri(url))     } }

Добавим загрузку видео внутри BubbleViewHolder.

BubbleViewHolder.kt

class BubbleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {      private val mediaSourceFactory = MediaSourceFactory(itemView.context)     private val playerView = itemView.findViewById<PlayerView>(R.id.li_bubble_player_view)     // плеер, отвечающий за взаимодействие с видео     private val player = ExoPlayer.Builder(itemView.context).build().apply {         //настройка повтора видео. В нашем случае воспроизводим одно видео по кругу         repeatMode = ExoPlayer.REPEAT_MODE_ONE     }      init {         playerView.player = player     }      fun bind(model: BubbleModel) {         val mediaSource = mediaSourceFactory.createMediaSource(model.videoUrl)         player.setMediaSource(mediaSource)         //начинает загрузку видео         player.prepare()         //начинаем воспроизведение как только видео загрузится         player.play()     } }

Заключение

Все готово! Запускаем проект и сразу видим несколько проблем:

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

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

В следующей части мы займемся исправлением этих проблем.

Читать далее: Часть 2. Оптимизация


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


Комментарии

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

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