Basic Video Support

This commit is contained in:
Valere 2020-07-08 20:09:39 +02:00
parent cc5df1e1d5
commit a1db8653ab
23 changed files with 537 additions and 75 deletions

View File

@ -62,6 +62,9 @@ dependencies {
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation "com.github.bumptech.glide:glide:4.10.0"
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.0'

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
sealed class AttachmentEvents {
data class VideoEvent(val isPlaying: Boolean, val progress: Int, val duration: Int) : AttachmentEvents()
}
interface AttachmentEventListener {
fun onEvent(event: AttachmentEvents)
}

View File

@ -22,7 +22,7 @@ import android.view.View
sealed class AttachmentInfo {
data class Image(val url: String, val data: Any?) : AttachmentInfo()
data class AnimatedImage(val url: String, val data: Any?) : AttachmentInfo()
data class Video(val url: String, val data: Any) : AttachmentInfo()
data class Video(val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo()
data class Audio(val url: String, val data: Any) : AttachmentInfo()
data class File(val url: String, val data: Any) : AttachmentInfo()
@ -40,5 +40,7 @@ interface AttachmentSourceProvider {
fun loadImage(holder: AnimatedImageViewHolder, info: AttachmentInfo.AnimatedImage)
fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video)
fun overlayViewAtPosition(context: Context, position: Int) : View?
}

View File

@ -34,15 +34,17 @@ import androidx.core.view.updatePadding
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.activity_attachment_viewer.*
import java.lang.ref.WeakReference
import kotlin.math.abs
abstract class AttachmentViewerActivity : AppCompatActivity() {
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
lateinit var pager2: ViewPager2
lateinit var imageTransitionView: ImageView
lateinit var transitionImageContainer: ViewGroup
var topInset = 0
var bottomInset = 0
var systemUiVisibility = true
private var overlayView: View? = null
@ -50,7 +52,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() {
if (value == overlayView) return
overlayView?.let { rootContainer.removeView(it) }
rootContainer.addView(value)
value?.updatePadding(top = topInset)
value?.updatePadding(top = topInset, bottom = bottomInset)
field = value
}
@ -109,8 +111,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity() {
}
override fun onPageSelected(position: Int) {
currentPosition = position
overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position)
onSelectedPositionChanged(position)
}
})
@ -121,12 +122,27 @@ abstract class AttachmentViewerActivity : AppCompatActivity() {
scaleDetector = createScaleGestureDetector()
ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets ->
overlayView?.updatePadding(top = insets.systemWindowInsetTop)
overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom)
topInset = insets.systemWindowInsetTop
bottomInset = insets.systemWindowInsetBottom
insets
}
}
fun onSelectedPositionChanged(position: Int) {
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
(it as? BaseViewHolder)?.onSelected(false)
}
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(position)?.let {
(it as? BaseViewHolder)?.onSelected(true)
if (it is VideoViewHolder) {
it.eventListener = WeakReference(this)
}
}
currentPosition = position
overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position)
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// The zoomable view is configured to disallow interception when image is zoomed
@ -264,6 +280,12 @@ abstract class AttachmentViewerActivity : AppCompatActivity() {
}
})
override fun onEvent(event: AttachmentEvents) {
if (overlayView is AttachmentEventListener) {
(overlayView as? AttachmentEventListener)?.onEvent(event)
}
}
protected open fun shouldAnimateDismiss(): Boolean = true
protected open fun animateClose() {

View File

@ -24,7 +24,11 @@ import androidx.recyclerview.widget.RecyclerView
abstract class BaseViewHolder constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) {
abstract fun bind(attachmentInfo: AttachmentInfo)
open fun bind(attachmentInfo: AttachmentInfo) {}
open fun onRecycled() {}
open fun onAttached() {}
open fun onDetached() {}
open fun onSelected(selected: Boolean) {}
}
class AttachmentViewHolder constructor(itemView: View) :
@ -59,6 +63,7 @@ class AttachmentsAdapter() : RecyclerView.Adapter<BaseViewHolder>() {
return when (viewType) {
R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView)
R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView)
R.layout.item_video_attachment -> VideoViewHolder(itemView)
else -> AttachmentViewHolder(itemView)
}
}
@ -88,11 +93,26 @@ class AttachmentsAdapter() : RecyclerView.Adapter<BaseViewHolder>() {
is AttachmentInfo.AnimatedImage -> {
attachmentSourceProvider?.loadImage(holder as AnimatedImageViewHolder, it)
}
is AttachmentInfo.Video -> {
attachmentSourceProvider?.loadVideo(holder as VideoViewHolder, it)
}
else -> {}
}
}
}
override fun onViewAttachedToWindow(holder: BaseViewHolder) {
holder.onAttached()
}
override fun onViewRecycled(holder: BaseViewHolder) {
holder.onRecycled()
}
override fun onViewDetachedFromWindow(holder: BaseViewHolder) {
holder.onDetached()
}
fun isScaled(position: Int): Boolean {
val holder = recyclerView?.findViewHolderForAdapterPosition(position)
if (holder is ZoomableImageViewHolder) {

View File

@ -0,0 +1,133 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.attachmentviewer
import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.VideoView
import androidx.core.view.isVisible
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import java.io.File
import java.lang.ref.WeakReference
import java.util.concurrent.TimeUnit
// TODO, it would be probably better to use a unique media player
// for better customization and control
// But for now VideoView is enough, it released player when detached, we use a timer to update progress
class VideoViewHolder constructor(itemView: View) :
BaseViewHolder(itemView) {
private var isSelected = false
private var mVideoPath: String? = null
private var progressDisposable: Disposable? = null
var eventListener: WeakReference<AttachmentEventListener>? = null
// interface Target {
// fun onResourceLoading(progress: Int, total: Int)
// fun onLoadFailed()
// fun onResourceReady(file: File)
// fun onThumbnailReady(thumbnail: Drawable?)
// }
init {
}
val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage)
val videoView: VideoView = itemView.findViewById(R.id.videoView)
val loaderProgressBar: ProgressBar = itemView.findViewById(R.id.videoLoaderProgress)
val videoControlIcon: ImageView = itemView.findViewById(R.id.videoControlIcon)
val errorTextView: TextView = itemView.findViewById(R.id.videoMediaViewerErrorView)
// val videoTarget = object : Target {
// override fun onResourceLoading(progress: Int, total: Int) {
// videoView.isVisible = false
// loaderProgressBar.isVisible = true
// }
//
// override fun onLoadFailed() {
// loaderProgressBar.isVisible = false
// }
//
// override fun onResourceReady(file: File) {
// }
//
// override fun onThumbnailReady(thumbnail: Drawable?) {
// }
// }
override fun onRecycled() {
super.onRecycled()
progressDisposable?.dispose()
progressDisposable = null
mVideoPath = null
}
fun videoReady(file: File) {
mVideoPath = file.path
if (isSelected) {
startPlaying()
}
}
override fun onSelected(selected: Boolean) {
if (!selected) {
if (videoView.isPlaying) {
videoView.stopPlayback()
progressDisposable?.dispose()
progressDisposable = null
}
} else {
if (mVideoPath != null) {
startPlaying()
}
}
isSelected = true
}
private fun startPlaying() {
thumbnailImage.isVisible = false
loaderProgressBar.isVisible = false
videoView.isVisible = true
videoView.setOnPreparedListener {
progressDisposable?.dispose()
progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS)
.timeInterval()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
val duration = videoView.duration
val progress = videoView.currentPosition
val isPlaying = videoView.isPlaying
Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
}
}
videoView.setVideoPath(mVideoPath)
videoView.start()
}
override fun bind(attachmentInfo: AttachmentInfo) {
Log.v("FOO", "")
}
}

View File

@ -1,7 +1,8 @@
<?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="match_parent">
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/videoThumbnailImage"
@ -14,13 +15,35 @@
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
android:layout_centerInParent="true" />
<ImageView
android:id="@+id/videoControlIcon"
android:layout_centerInParent="true"
android:visibility="gone"
tools:visibility="visible"
android:layout_width="44dp"
android:layout_height="44dp"
/>
<ProgressBar
android:layout_centerInParent="true"
android:id="@+id/videoLoaderProgress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="invisible"
tools:visibility="visible" />
<TextView
android:id="@+id/videoMediaViewerErrorView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_margin="16dp"
android:textSize="16sp"
android:visibility="gone"
tools:text="Error"
tools:visibility="visible" />
</RelativeLayout>

View File

@ -40,5 +40,5 @@ interface TimelineService {
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
fun getAttachementMessages() : List<TimelineEvent>
fun getAttachmentMessages() : List<TimelineEvent>
}

View File

@ -22,6 +22,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isVideoMessage
import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineService
@ -94,14 +95,14 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
}
}
override fun getAttachementMessages(): List<TimelineEvent> {
override fun getAttachmentMessages(): List<TimelineEvent> {
// TODO pretty bad query.. maybe we should denormalize clear type in base?
return doWithRealm(monarchy.realmConfiguration) { realm ->
realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
.findAll()
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() } }
?.mapNotNull { timelineEventMapper.map(it).takeIf { it.root.isImageMessage() || it.root.isVideoMessage() } }
?: emptyList()
}
}

View File

@ -1178,7 +1178,10 @@ class RoomDetailFragment @Inject constructor(
}
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
navigator.openVideoViewer(requireActivity(), mediaData)
navigator.openVideoViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs ->
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
}
}
// override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {

View File

@ -337,7 +337,7 @@ class MessageItemFactory @Inject constructor(
.playable(true)
.highlighted(highlight)
.mediaData(thumbnailData)
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view) }
.clickListener { view -> callback?.onVideoMessageClicked(messageContent, videoData, view.findViewById(R.id.messageThumbnailView)) }
}
private fun buildItemForTextContent(messageContent: MessageTextContent,

View File

@ -21,20 +21,28 @@ import android.graphics.Color
import android.util.AttributeSet
import android.view.View
import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Group
import im.vector.riotx.R
import im.vector.riotx.attachmentviewer.AttachmentEventListener
import im.vector.riotx.attachmentviewer.AttachmentEvents
class AttachmentOverlayView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
) : ConstraintLayout(context, attrs, defStyleAttr), AttachmentEventListener {
var onShareCallback: (() -> Unit) ? = null
var onBack: (() -> Unit) ? = null
var onShareCallback: (() -> Unit)? = null
var onBack: (() -> Unit)? = null
private val counterTextView: TextView
private val infoTextView: TextView
private val shareImage: ImageView
private val overlayPlayPauseButton: ImageView
private val overlaySeekBar: SeekBar
val videoControlsGroup: Group
init {
View.inflate(context, R.layout.merge_image_attachment_overlay, this)
@ -42,14 +50,29 @@ class AttachmentOverlayView @JvmOverloads constructor(
counterTextView = findViewById(R.id.overlayCounterText)
infoTextView = findViewById(R.id.overlayInfoText)
shareImage = findViewById(R.id.overlayShareButton)
videoControlsGroup = findViewById(R.id.overlayVideoControlsGroup)
overlayPlayPauseButton = findViewById(R.id.overlayPlayPauseButton)
overlaySeekBar = findViewById(R.id.overlaySeekBar)
overlaySeekBar.isEnabled = false
findViewById<ImageView>(R.id.overlayBackButton).setOnClickListener {
onBack?.invoke()
}
}
fun updateWith(counter: String, senderInfo : String) {
fun updateWith(counter: String, senderInfo: String) {
counterTextView.text = counter
infoTextView.text = senderInfo
}
override fun onEvent(event: AttachmentEvents) {
when (event) {
is AttachmentEvents.VideoEvent -> {
overlayPlayPauseButton.setImageResource(if (!event.isPlaying) R.drawable.ic_play_arrow else R.drawable.ic_pause)
val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat()
val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100)
overlaySeekBar.progress = percent
}
}
}
}

View File

@ -44,21 +44,29 @@ import java.io.File
import javax.inject.Inject
import kotlin.math.min
interface AttachmentData : Parcelable {
val eventId: String
val filename: String
val mimeType: String?
val url: String?
val elementToDecrypt: ElementToDecrypt?
}
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val dimensionConverter: DimensionConverter) {
@Parcelize
data class Data(
val eventId: String,
val filename: String,
val mimeType: String?,
val url: String?,
val elementToDecrypt: ElementToDecrypt?,
override val eventId: String,
override val filename: String,
override val mimeType: String?,
override val url: String?,
override val elementToDecrypt: ElementToDecrypt?,
val height: Int?,
val maxHeight: Int,
val width: Int?,
val maxWidth: Int
) : Parcelable {
) : AttachmentData {
fun isLocalFile() = url.isLocalFile()
}

View File

@ -19,9 +19,18 @@ package im.vector.riotx.features.media
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import androidx.core.view.isVisible
import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.transition.Transition
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.isVideoMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
@ -29,16 +38,20 @@ import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.riotx.attachmentviewer.AnimatedImageViewHolder
import im.vector.riotx.attachmentviewer.AttachmentInfo
import im.vector.riotx.attachmentviewer.AttachmentSourceProvider
import im.vector.riotx.attachmentviewer.VideoViewHolder
import im.vector.riotx.attachmentviewer.ZoomableImageViewHolder
import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.extensions.localDateTime
import java.io.File
import javax.inject.Inject
class RoomAttachmentProvider(
private val attachments: List<TimelineEvent>,
private val initialIndex: Int,
private val imageContentRenderer: ImageContentRenderer,
private val dateFormatter: VectorDateFormatter
private val videoContentRenderer: VideoContentRenderer,
private val dateFormatter: VectorDateFormatter,
private val fileService: FileService
) : AttachmentSourceProvider {
interface InteractionListener {
@ -57,26 +70,64 @@ class RoomAttachmentProvider(
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
return attachments[position].let {
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent
val data = ImageContentRenderer.Data(
eventId = it.eventId,
filename = content?.body ?: "",
mimeType = content?.mimeType,
url = content?.getFileUrl(),
elementToDecrypt = content?.encryptedFileInfo?.toElementToDecrypt(),
maxHeight = -1,
maxWidth = -1,
width = null,
height = null
)
if (content?.mimeType == "image/gif") {
AttachmentInfo.AnimatedImage(
content.url ?: "",
data
if (content is MessageImageContent) {
val data = ImageContentRenderer.Data(
eventId = it.eventId,
filename = content.body,
mimeType = content.mimeType,
url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
maxHeight = -1,
maxWidth = -1,
width = null,
height = null
)
if (content.mimeType == "image/gif") {
AttachmentInfo.AnimatedImage(
content.url ?: "",
data
)
} else {
AttachmentInfo.Image(
content.url ?: "",
data
)
}
} else if (content is MessageVideoContent) {
val thumbnailData = ImageContentRenderer.Data(
eventId = it.eventId,
filename = content.body,
mimeType = content.mimeType,
url = content.videoInfo?.thumbnailFile?.url
?: content.videoInfo?.thumbnailUrl,
elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = content.videoInfo?.height,
maxHeight = -1,
width = content.videoInfo?.width,
maxWidth = -1
)
val data = VideoContentRenderer.Data(
eventId = it.eventId,
filename = content.body,
mimeType = content.mimeType,
url = content.getFileUrl(),
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
thumbnailMediaData = thumbnailData
)
AttachmentInfo.Video(
content.getFileUrl() ?: "",
data,
AttachmentInfo.Image(
url = content.videoInfo?.thumbnailFile?.url
?: content.videoInfo?.thumbnailUrl ?: "",
data = thumbnailData
)
)
} else {
AttachmentInfo.Image(
content?.url ?: "",
data
"",
null
)
}
}
@ -94,6 +145,49 @@ class RoomAttachmentProvider(
}
}
override fun loadVideo(holder: VideoViewHolder, info: AttachmentInfo.Video) {
val data = info.data as? VideoContentRenderer.Data ?: return
// videoContentRenderer.render(data,
// holder.thumbnailImage,
// holder.loaderProgressBar,
// holder.videoView,
// holder.errorTextView)
imageContentRenderer.render(data.thumbnailMediaData, holder.thumbnailImage, object : CustomViewTarget<ImageView, Drawable>(holder.thumbnailImage) {
override fun onLoadFailed(errorDrawable: Drawable?) {
holder.thumbnailImage.setImageDrawable(errorDrawable)
}
override fun onResourceCleared(placeholder: Drawable?) {
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
holder.thumbnailImage.setImageDrawable(resource)
}
})
holder.thumbnailImage.isVisible = false
holder.loaderProgressBar.isVisible = false
holder.videoView.isVisible = false
fileService.downloadFile(
downloadMode = FileService.DownloadMode.FOR_INTERNAL_USE,
id = data.eventId,
mimeType = data.mimeType,
elementToDecrypt = data.elementToDecrypt,
fileName = data.filename,
url = data.url,
callback = object : MatrixCallback<File> {
override fun onSuccess(data: File) {
holder.videoReady(data)
}
override fun onFailure(failure: Throwable) {
holder.videoView.isVisible = false
}
}
)
}
override fun overlayViewAtPosition(context: Context, position: Int): View? {
if (overlayView == null) {
overlayView = AttachmentOverlayView(context)
@ -109,22 +203,19 @@ class RoomAttachmentProvider(
"${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
}
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString")
overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()
return overlayView
}
// override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) {
// (info.data as? ImageContentRenderer.Data)?.let {
// imageContentRenderer.render(it, ImageContentRenderer.Mode.FULL_SIZE, holder.touchImageView)
// }
// }
}
class RoomAttachmentProviderFactory @Inject constructor(
private val imageContentRenderer: ImageContentRenderer,
private val vectorDateFormatter: VectorDateFormatter
private val vectorDateFormatter: VectorDateFormatter,
private val videoContentRenderer: VideoContentRenderer,
private val session: Session
) {
fun createProvider(attachments: List<TimelineEvent>, initialIndex: Int): RoomAttachmentProvider {
return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter)
return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer, videoContentRenderer, vectorDateFormatter, session.fileService())
}
}

View File

@ -80,7 +80,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen
val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
val room = args.roomId?.let { session.getRoom(it) }
val events = room?.getAttachementMessages() ?: emptyList()
val events = room?.getAttachmentMessages() ?: emptyList()
val index = events.indexOfFirst { it.eventId == args.eventId }
initialIndex = index
@ -90,8 +90,8 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen
transitionImageContainer.isVisible = true
// Postpone transaction a bit until thumbnail is loaded
val mediaData: ImageContentRenderer.Data? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
if (mediaData != null) {
val mediaData: Parcelable? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
if (mediaData is ImageContentRenderer.Data) {
// will be shown at end of transition
pager2.isInvisible = true
supportPostponeEnterTransition()
@ -99,6 +99,14 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen
// Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView)
}
} else if (mediaData is VideoContentRenderer.Data) {
// will be shown at end of transition
pager2.isInvisible = true
supportPostponeEnterTransition()
imageContentRenderer.renderThumbnailDontTransform(mediaData.thumbnailMediaData, imageTransitionView) {
// Proceed with transaction
scheduleStartPostponedTransition(imageTransitionView)
}
}
}
}
@ -108,6 +116,10 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen
setSourceProvider(sourceProvider)
if (savedInstanceState == null) {
pager2.setCurrentItem(index, false)
// The page change listener is not notified of the change...
pager2.post {
onSelectedPositionChanged(index)
}
}
window.statusBarColor = ContextCompat.getColor(this, R.color.black_alpha)
@ -202,7 +214,7 @@ class VectorAttachmentViewerActivity : AttachmentViewerActivity(), RoomAttachmen
const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
fun newIntent(context: Context,
mediaData: ImageContentRenderer.Data,
mediaData: AttachmentData,
roomId: String?,
eventId: String,
sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.media
import android.os.Parcelable
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
@ -38,13 +37,13 @@ class VideoContentRenderer @Inject constructor(private val activeSessionHolder:
@Parcelize
data class Data(
val eventId: String,
val filename: String,
val mimeType: String?,
val url: String?,
val elementToDecrypt: ElementToDecrypt?,
override val eventId: String,
override val filename: String,
override val mimeType: String?,
override val url: String?,
override val elementToDecrypt: ElementToDecrypt?,
val thumbnailMediaData: ImageContentRenderer.Data
) : Parcelable
) : AttachmentData
fun render(data: Data,
thumbnailView: ImageView,

View File

@ -49,11 +49,10 @@ import im.vector.riotx.features.home.room.detail.RoomDetailArgs
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.media.BigImageViewerActivity
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VectorAttachmentViewerActivity
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
@ -248,7 +247,7 @@ class DefaultNavigator @Inject constructor(
override fun openImageViewer(activity: Activity,
roomId: String?,
mediaData: ImageContentRenderer.Data,
mediaData: AttachmentData,
view: View,
options: ((MutableList<Pair<View, String>>) -> Unit)?) {
VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent ->
@ -284,9 +283,28 @@ class DefaultNavigator @Inject constructor(
// activity.startActivity(intent, bundle)
}
override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) {
val intent = VideoMediaViewerActivity.newIntent(activity, mediaData)
activity.startActivity(intent)
override fun openVideoViewer(activity: Activity,
roomId: String?, mediaData: VideoContentRenderer.Data,
view: View,
options: ((MutableList<Pair<View, String>>) -> Unit)?) {
// val intent = VideoMediaViewerActivity.newIntent(activity, mediaData)
// activity.startActivity(intent)
VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent ->
val pairs = ArrayList<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
activity.window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
}
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
options?.invoke(pairs)
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
activity.startActivity(intent, bundle)
}
}
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {

View File

@ -26,7 +26,7 @@ import im.vector.matrix.android.api.session.terms.TermsService
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.AttachmentData
import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.SharedData
@ -93,9 +93,12 @@ interface Navigator {
fun openImageViewer(activity: Activity,
roomId: String?,
mediaData: ImageContentRenderer.Data,
mediaData: AttachmentData,
view: View,
options: ((MutableList<Pair<View, String>>) -> Unit)?)
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
fun openVideoViewer(activity: Activity,
roomId: String?, mediaData: VideoContentRenderer.Data,
view: View,
options: ((MutableList<Pair<View, String>>) -> Unit)?)
}

View File

@ -81,7 +81,8 @@ class RoomUploadsMediaFragment @Inject constructor(
}
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
navigator.openVideoViewer(requireActivity(), mediaData)
// TODO
// navigator.openVideoViewer(requireActivity(), mediaData, null, )
}
override fun loadMore() {

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@ -10,7 +10,8 @@
android:id="@+id/overlayTopBackground"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/black_alpha">
android:background="@color/black_alpha"
app:layout_constraintTop_toTopOf="parent">
</View>
@ -21,12 +22,12 @@
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:scaleType="centerInside"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/share"
android:focusable="true"
android:padding="6dp"
android:scaleType="centerInside"
android:tint="@color/white"
app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground"
app:layout_constraintStart_toStartOf="@id/overlayTopBackground"
@ -72,6 +73,8 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/share"
android:padding="6dp"
android:tint="@color/white"
@ -80,4 +83,51 @@
app:layout_constraintTop_toTopOf="@id/overlayTopBackground"
app:srcCompat="@drawable/ic_share" />
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/overlayVideoControlsGroup"
android:visibility="gone"
tools:visibility="visible"
app:constraint_referenced_ids="overlayBottomBackground,overlayBackButton,overlayPlayPauseButton,overlaySeekBar" />
<View
android:id="@+id/overlayBottomBackground"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/black_alpha"
app:layout_constraintBottom_toBottomOf="parent" />
<ImageView
android:id="@+id/overlayPlayPauseButton"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:contentDescription="@string/play_video"
android:focusable="true"
android:padding="6dp"
android:scaleType="centerInside"
android:tint="@color/white"
app:layout_constraintBottom_toBottomOf="@id/overlayBottomBackground"
app:layout_constraintStart_toStartOf="@id/overlayBottomBackground"
app:layout_constraintTop_toTopOf="@id/overlayBottomBackground"
app:srcCompat="@drawable/ic_play_arrow" />
<SeekBar
android:id="@+id/overlaySeekBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:backgroundTint="@color/white"
android:progressBackgroundTint="@color/white"
android:thumbTint="@color/white"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@id/overlayBottomBackground"
app:layout_constraintEnd_toEndOf="@id/overlayBottomBackground"
app:layout_constraintStart_toEndOf="@id/overlayPlayPauseButton"
app:layout_constraintTop_toTopOf="@id/overlayBottomBackground" />
</merge>

View File

@ -76,6 +76,10 @@
<string name="disconnect">Disconnect</string>
<string name="report_content">Report content</string>
<string name="active_call">Active call</string>
<string name="play_video">Play</string>
<string name="pause_video">Pause</string>
<!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video -->
<string name="ongoing_conference_call">Ongoing conference call.\nJoin as %1$s or %2$s</string>
<string name="ongoing_conference_call_voice">Voice</string>