Видео-сообщение как в Telegram. Часть первая — Список видео
Часто ли вы пользуетесь 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. Оптимизация