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

f5349f0114ed57f1db9fa505af183285.png

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

Оглавление

Введение

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

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

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

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

activity_main.xml




    

Создаем 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




    

  • 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(R.id.li_bubble_player_view)

    fun bind(model: BubbleModel) {
      // реализация будет ниже
    }
}

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

BubbleAdapter.kt

class BubbleAdapter(
    private val items: List
) : RecyclerView.Adapter() {

    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().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(R.id.bubbles_rv)
        recyclerView.adapter = BubbleAdapter(items)
    }
}

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

AndroidManifest.xml




    

    

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

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(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. Оптимизация

© Habrahabr.ru