mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
Merge pull request #1636 from vector-im/feature/attachement_pager
Feature/attachement pager
This commit is contained in:
commit
8582ad6015
@ -8,6 +8,7 @@ Improvements 🙌:
|
||||
- Cleaning chunks with lots of events as long as a threshold has been exceeded (35_000 events in DB) (#1634)
|
||||
- Creating and listening to EventInsertEntity. (#1634)
|
||||
- Handling (almost) properly the groups fetching (#1634)
|
||||
- Improve fullscreen media display (#327)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Regression | Share action menu do not work (#1647)
|
||||
|
1
attachment-viewer/.gitignore
vendored
Normal file
1
attachment-viewer/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/build
|
78
attachment-viewer/build.gradle
Normal file
78
attachment-viewer/build.gradle
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
content {
|
||||
// PhotoView
|
||||
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||
}
|
||||
}
|
||||
jcenter()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.0.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'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
}
|
0
attachment-viewer/consumer-rules.pro
Normal file
0
attachment-viewer/consumer-rules.pro
Normal file
21
attachment-viewer/proguard-rules.pro
vendored
Normal file
21
attachment-viewer/proguard-rules.pro
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
attachment-viewer/src/main/AndroidManifest.xml
Normal file
2
attachment-viewer/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="im.vector.riotx.attachmentviewer" />
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
|
||||
class AnimatedImageViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView) {
|
||||
|
||||
val touchImageView: ImageView = itemView.findViewById(R.id.imageView)
|
||||
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
|
||||
|
||||
internal val target = DefaultImageLoaderTarget(this, this.touchImageView)
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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)
|
||||
}
|
||||
|
||||
sealed class AttachmentCommands {
|
||||
object PauseVideo : AttachmentCommands()
|
||||
object StartVideo : AttachmentCommands()
|
||||
data class SeekTo(val percentProgress: Int) : AttachmentCommands()
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.content.Context
|
||||
import android.view.View
|
||||
|
||||
sealed class AttachmentInfo(open val uid: String) {
|
||||
data class Image(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid)
|
||||
data class AnimatedImage(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid)
|
||||
data class Video(override val uid: String, val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo(uid)
|
||||
// data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid)
|
||||
// data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid)
|
||||
}
|
||||
|
||||
interface AttachmentSourceProvider {
|
||||
|
||||
fun getItemCount(): Int
|
||||
|
||||
fun getAttachmentInfoAt(position: Int): AttachmentInfo
|
||||
|
||||
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image)
|
||||
|
||||
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage)
|
||||
|
||||
fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video)
|
||||
|
||||
fun overlayViewAtPosition(context: Context, position: Int): View?
|
||||
|
||||
fun clear(id: String)
|
||||
}
|
@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* 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.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
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(), 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
|
||||
set(value) {
|
||||
if (value == overlayView) return
|
||||
overlayView?.let { rootContainer.removeView(it) }
|
||||
rootContainer.addView(value)
|
||||
value?.updatePadding(top = topInset, bottom = bottomInset)
|
||||
field = value
|
||||
}
|
||||
|
||||
private lateinit var swipeDismissHandler: SwipeToDismissHandler
|
||||
private lateinit var directionDetector: SwipeDirectionDetector
|
||||
private lateinit var scaleDetector: ScaleGestureDetector
|
||||
private lateinit var gestureDetector: GestureDetectorCompat
|
||||
|
||||
var currentPosition = 0
|
||||
|
||||
private var swipeDirection: SwipeDirection? = null
|
||||
|
||||
private fun isScaled() = attachmentsAdapter.isScaled(currentPosition)
|
||||
|
||||
private var wasScaled: Boolean = false
|
||||
private var isSwipeToDismissAllowed: Boolean = true
|
||||
private lateinit var attachmentsAdapter: AttachmentsAdapter
|
||||
private var isOverlayWasClicked = false
|
||||
|
||||
// private val shouldDismissToBottom: Boolean
|
||||
// get() = e == null
|
||||
// || !externalTransitionImageView.isRectVisible
|
||||
// || !isAtStartPosition
|
||||
|
||||
private var isImagePagerIdle = true
|
||||
|
||||
fun setSourceProvider(sourceProvider: AttachmentSourceProvider) {
|
||||
attachmentsAdapter.attachmentSourceProvider = sourceProvider
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// This is important for the dispatchTouchEvent, if not we must correct
|
||||
// the touch coordinates
|
||||
window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE)
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
||||
|
||||
setContentView(R.layout.activity_attachment_viewer)
|
||||
attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
|
||||
attachmentsAdapter = AttachmentsAdapter()
|
||||
attachmentPager.adapter = attachmentsAdapter
|
||||
imageTransitionView = transitionImageView
|
||||
transitionImageContainer = findViewById(R.id.transitionImageContainer)
|
||||
pager2 = attachmentPager
|
||||
directionDetector = createSwipeDirectionDetector()
|
||||
gestureDetector = createGestureDetector()
|
||||
|
||||
attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
onSelectedPositionChanged(position)
|
||||
}
|
||||
})
|
||||
|
||||
swipeDismissHandler = createSwipeToDismissHandler()
|
||||
rootContainer.setOnTouchListener(swipeDismissHandler)
|
||||
rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 }
|
||||
|
||||
scaleDetector = createScaleGestureDetector()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets ->
|
||||
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 onPause() {
|
||||
attachmentsAdapter.onPause(currentPosition)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
attachmentsAdapter.onResume(currentPosition)
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
// The zoomable view is configured to disallow interception when image is zoomed
|
||||
|
||||
// Check if the overlay is visible, and wants to handle the click
|
||||
if (overlayView?.isVisible == true && overlayView?.dispatchTouchEvent(ev) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev")
|
||||
handleUpDownEvent(ev)
|
||||
|
||||
// Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}")
|
||||
// Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}")
|
||||
// Log.v("ATTACHEMENTS", "wasScaled $wasScaled")
|
||||
if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) {
|
||||
wasScaled = true
|
||||
// Log.v("ATTACHEMENTS", "dispatch to pager")
|
||||
return attachmentPager.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
// Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}")
|
||||
return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also {
|
||||
// Log.v("ATTACHEMENTS", "\n================")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpDownEvent(event: MotionEvent) {
|
||||
// Log.v("ATTACHEMENTS", "handleUpDownEvent $event")
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
handleEventActionUp(event)
|
||||
}
|
||||
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
handleEventActionDown(event)
|
||||
}
|
||||
|
||||
scaleDetector.onTouchEvent(event)
|
||||
gestureDetector.onTouchEvent(event)
|
||||
}
|
||||
|
||||
private fun handleEventActionDown(event: MotionEvent) {
|
||||
swipeDirection = null
|
||||
wasScaled = false
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
isOverlayWasClicked = dispatchOverlayTouch(event)
|
||||
}
|
||||
|
||||
private fun handleEventActionUp(event: MotionEvent) {
|
||||
// wasDoubleTapped = false
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
isOverlayWasClicked = dispatchOverlayTouch(event)
|
||||
}
|
||||
|
||||
private fun handleSingleTap(event: MotionEvent, isOverlayWasClicked: Boolean) {
|
||||
// TODO if there is no overlay, we should at least toggle system bars?
|
||||
if (overlayView != null && !isOverlayWasClicked) {
|
||||
toggleOverlayViewVisibility()
|
||||
super.dispatchTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleOverlayViewVisibility() {
|
||||
if (systemUiVisibility) {
|
||||
// we hide
|
||||
TransitionManager.beginDelayedTransition(rootContainer)
|
||||
hideSystemUI()
|
||||
overlayView?.isVisible = false
|
||||
} else {
|
||||
// we show
|
||||
TransitionManager.beginDelayedTransition(rootContainer)
|
||||
showSystemUI()
|
||||
overlayView?.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTouchIfNotScaled(event: MotionEvent): Boolean {
|
||||
// Log.v("ATTACHEMENTS", "handleTouchIfNotScaled $event")
|
||||
directionDetector.handleTouchEvent(event)
|
||||
|
||||
return when (swipeDirection) {
|
||||
SwipeDirection.Up, SwipeDirection.Down -> {
|
||||
if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) {
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
} else true
|
||||
}
|
||||
SwipeDirection.Left, SwipeDirection.Right -> {
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) {
|
||||
val alpha = calculateTranslationAlpha(translationY, translationLimit)
|
||||
backgroundView.alpha = alpha
|
||||
dismissContainer.alpha = alpha
|
||||
overlayView?.alpha = alpha
|
||||
}
|
||||
|
||||
private fun dispatchOverlayTouch(event: MotionEvent): Boolean =
|
||||
overlayView
|
||||
?.let { it.isVisible && it.dispatchTouchEvent(event) }
|
||||
?: false
|
||||
|
||||
private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float =
|
||||
1.0f - 1.0f / translationLimit.toFloat() / 4f * abs(translationY)
|
||||
|
||||
private fun createSwipeToDismissHandler()
|
||||
: SwipeToDismissHandler = SwipeToDismissHandler(
|
||||
swipeView = dismissContainer,
|
||||
shouldAnimateDismiss = { shouldAnimateDismiss() },
|
||||
onDismiss = { animateClose() },
|
||||
onSwipeViewMove = ::handleSwipeViewMove)
|
||||
|
||||
private fun createSwipeDirectionDetector() =
|
||||
SwipeDirectionDetector(this) { swipeDirection = it }
|
||||
|
||||
private fun createScaleGestureDetector() =
|
||||
ScaleGestureDetector(this, ScaleGestureDetector.SimpleOnScaleGestureListener())
|
||||
|
||||
private fun createGestureDetector() =
|
||||
GestureDetectorCompat(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (isImagePagerIdle) {
|
||||
handleSingleTap(e, isOverlayWasClicked)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||
return super.onDoubleTap(e)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onEvent(event: AttachmentEvents) {
|
||||
if (overlayView is AttachmentEventListener) {
|
||||
(overlayView as? AttachmentEventListener)?.onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun shouldAnimateDismiss(): Boolean = true
|
||||
|
||||
protected open fun animateClose() {
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
finish()
|
||||
}
|
||||
|
||||
fun handle(commands: AttachmentCommands) {
|
||||
(attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)
|
||||
?.handleCommand(commands)
|
||||
}
|
||||
|
||||
private fun hideSystemUI() {
|
||||
systemUiVisibility = false
|
||||
// Enables regular immersive mode.
|
||||
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
|
||||
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
// Set the content to appear under the system bars so that the
|
||||
// content doesn't resize when the system bars hide and show.
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
// Hide the nav bar and status bar
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||
}
|
||||
|
||||
// Shows the system bars by removing all the flags
|
||||
// except for the ones that make the content appear under the system bars.
|
||||
private fun showSystemUI() {
|
||||
systemUiVisibility = true
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class AttachmentsAdapter : RecyclerView.Adapter<BaseViewHolder>() {
|
||||
|
||||
var attachmentSourceProvider: AttachmentSourceProvider? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
this.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
this.recyclerView = null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val itemView = inflater.inflate(viewType, parent, false)
|
||||
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 -> UnsupportedViewHolder(itemView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val info = attachmentSourceProvider!!.getAttachmentInfoAt(position)
|
||||
return when (info) {
|
||||
is AttachmentInfo.Image -> R.layout.item_image_attachment
|
||||
is AttachmentInfo.Video -> R.layout.item_video_attachment
|
||||
is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment
|
||||
// is AttachmentInfo.Audio -> TODO()
|
||||
// is AttachmentInfo.File -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return attachmentSourceProvider?.getItemCount() ?: 0
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
attachmentSourceProvider?.getAttachmentInfoAt(position)?.let {
|
||||
holder.bind(it)
|
||||
when (it) {
|
||||
is AttachmentInfo.Image -> {
|
||||
attachmentSourceProvider?.loadImage((holder as ZoomableImageViewHolder).target, it)
|
||||
}
|
||||
is AttachmentInfo.AnimatedImage -> {
|
||||
attachmentSourceProvider?.loadImage((holder as AnimatedImageViewHolder).target, it)
|
||||
}
|
||||
is AttachmentInfo.Video -> {
|
||||
attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, 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) {
|
||||
return holder.touchImageView.attacher.scale > 1f
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun onPause(position: Int) {
|
||||
val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder
|
||||
holder?.entersBackground()
|
||||
}
|
||||
|
||||
fun onResume(position: Int) {
|
||||
val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder
|
||||
holder?.entersForeground()
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class BaseViewHolder constructor(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
open fun onRecycled() {
|
||||
boundResourceUid = null
|
||||
}
|
||||
|
||||
open fun onAttached() {}
|
||||
open fun onDetached() {}
|
||||
open fun entersBackground() {}
|
||||
open fun entersForeground() {}
|
||||
open fun onSelected(selected: Boolean) {}
|
||||
|
||||
open fun handleCommand(commands: AttachmentCommands) {}
|
||||
|
||||
var boundResourceUid: String? = null
|
||||
|
||||
open fun bind(attachmentInfo: AttachmentInfo) {
|
||||
boundResourceUid = attachmentInfo.uid
|
||||
}
|
||||
}
|
||||
|
||||
class UnsupportedViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView)
|
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
|
||||
interface ImageLoaderTarget {
|
||||
|
||||
fun contextView(): ImageView
|
||||
|
||||
fun onResourceLoading(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onLoadFailed(uid: String, errorDrawable: Drawable?)
|
||||
|
||||
fun onResourceCleared(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onResourceReady(uid: String, resource: Drawable)
|
||||
}
|
||||
|
||||
internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, private val contextView: ImageView)
|
||||
: ImageLoaderTarget {
|
||||
override fun contextView(): ImageView {
|
||||
return contextView
|
||||
}
|
||||
|
||||
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.touchImageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
// Glide mess up the view size :/
|
||||
holder.touchImageView.updateLayoutParams {
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
holder.touchImageView.setImageDrawable(resource)
|
||||
if (resource is Animatable) {
|
||||
resource.start()
|
||||
}
|
||||
}
|
||||
|
||||
internal class ZoomableImageTarget(val holder: ZoomableImageViewHolder, private val contextView: ImageView) : ImageLoaderTarget {
|
||||
override fun contextView() = contextView
|
||||
|
||||
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.touchImageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
// Glide mess up the view size :/
|
||||
holder.touchImageView.updateLayoutParams {
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
holder.touchImageView.setImageDrawable(resource)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* 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 SwipeDirection {
|
||||
object NotDetected : SwipeDirection()
|
||||
object Up : SwipeDirection()
|
||||
object Down : SwipeDirection()
|
||||
object Left : SwipeDirection()
|
||||
object Right : SwipeDirection()
|
||||
|
||||
companion object {
|
||||
fun fromAngle(angle: Double): SwipeDirection {
|
||||
return when (angle) {
|
||||
in 0.0..45.0 -> Right
|
||||
in 45.0..135.0 -> Up
|
||||
in 135.0..225.0 -> Left
|
||||
in 225.0..315.0 -> Down
|
||||
in 315.0..360.0 -> Right
|
||||
else -> NotDetected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* 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.content.Context
|
||||
import android.view.MotionEvent
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class SwipeDirectionDetector(
|
||||
context: Context,
|
||||
private val onDirectionDetected: (SwipeDirection) -> Unit
|
||||
) {
|
||||
|
||||
private val touchSlop: Int = android.view.ViewConfiguration.get(context).scaledTouchSlop
|
||||
private var startX: Float = 0f
|
||||
private var startY: Float = 0f
|
||||
private var isDetected: Boolean = false
|
||||
|
||||
fun handleTouchEvent(event: MotionEvent) {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
||||
if (!isDetected) {
|
||||
onDirectionDetected(SwipeDirection.NotDetected)
|
||||
}
|
||||
startY = 0.0f
|
||||
startX = startY
|
||||
isDetected = false
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> if (!isDetected && getEventDistance(event) > touchSlop) {
|
||||
isDetected = true
|
||||
onDirectionDetected(getDirection(startX, startY, event.x, event.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method
|
||||
* returns the direction that an arrow pointing from p1 to p2 would have.
|
||||
*
|
||||
* @param x1 the x position of the first point
|
||||
* @param y1 the y position of the first point
|
||||
* @param x2 the x position of the second point
|
||||
* @param y2 the y position of the second point
|
||||
* @return the direction
|
||||
*/
|
||||
private fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): SwipeDirection {
|
||||
val angle = getAngle(x1, y1, x2, y2)
|
||||
return SwipeDirection.fromAngle(angle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the angle between two points in the plane (x1,y1) and (x2, y2)
|
||||
* The angle is measured with 0/360 being the X-axis to the right, angles
|
||||
* increase counter clockwise.
|
||||
*
|
||||
* @param x1 the x position of the first point
|
||||
* @param y1 the y position of the first point
|
||||
* @param x2 the x position of the second point
|
||||
* @param y2 the y position of the second point
|
||||
* @return the angle between two points
|
||||
*/
|
||||
private fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double {
|
||||
val rad = Math.atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI
|
||||
return (rad * 180 / Math.PI + 180) % 360
|
||||
}
|
||||
|
||||
private fun getEventDistance(ev: MotionEvent): Float {
|
||||
val dx = ev.getX(0) - startX
|
||||
val dy = ev.getY(0) - startY
|
||||
return sqrt((dx * dx + dy * dy).toDouble()).toFloat()
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* 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.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Rect
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
|
||||
class SwipeToDismissHandler(
|
||||
private val swipeView: View,
|
||||
private val onDismiss: () -> Unit,
|
||||
private val onSwipeViewMove: (translationY: Float, translationLimit: Int) -> Unit,
|
||||
private val shouldAnimateDismiss: () -> Boolean
|
||||
) : View.OnTouchListener {
|
||||
|
||||
companion object {
|
||||
private const val ANIMATION_DURATION = 200L
|
||||
}
|
||||
|
||||
var translationLimit: Int = swipeView.height / 4
|
||||
private var isTracking = false
|
||||
private var startY: Float = 0f
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (swipeView.hitRect.contains(event.x.toInt(), event.y.toInt())) {
|
||||
isTracking = true
|
||||
}
|
||||
startY = event.y
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
if (isTracking) {
|
||||
isTracking = false
|
||||
onTrackingEnd(v.height)
|
||||
}
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (isTracking) {
|
||||
val translationY = event.y - startY
|
||||
swipeView.translationY = translationY
|
||||
onSwipeViewMove(translationY, translationLimit)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun initiateDismissToBottom() {
|
||||
animateTranslation(swipeView.height.toFloat())
|
||||
}
|
||||
|
||||
private fun onTrackingEnd(parentHeight: Int) {
|
||||
val animateTo = when {
|
||||
swipeView.translationY < -translationLimit -> -parentHeight.toFloat()
|
||||
swipeView.translationY > translationLimit -> parentHeight.toFloat()
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
if (animateTo != 0f && !shouldAnimateDismiss()) {
|
||||
onDismiss()
|
||||
} else {
|
||||
animateTranslation(animateTo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateTranslation(translationTo: Float) {
|
||||
swipeView.animate()
|
||||
.translationY(translationTo)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.setInterpolator(AccelerateInterpolator())
|
||||
.setUpdateListener { onSwipeViewMove(swipeView.translationY, translationLimit) }
|
||||
.setAnimatorListener(onAnimationEnd = {
|
||||
if (translationTo != 0f) {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// remove the update listener, otherwise it will be saved on the next animation execution:
|
||||
swipeView.animate().setUpdateListener(null)
|
||||
})
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewPropertyAnimator.setAnimatorListener(
|
||||
onAnimationEnd: ((Animator?) -> Unit)? = null,
|
||||
onAnimationStart: ((Animator?) -> Unit)? = null
|
||||
) = this.setListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
onAnimationEnd?.invoke(animation)
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
onAnimationStart?.invoke(animation)
|
||||
}
|
||||
})
|
||||
|
||||
internal val View?.hitRect: Rect
|
||||
get() = Rect().also { this?.getHitRect(it) }
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.isVisible
|
||||
import java.io.File
|
||||
|
||||
interface VideoLoaderTarget {
|
||||
fun contextView(): ImageView
|
||||
|
||||
fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?)
|
||||
|
||||
fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onThumbnailResourceReady(uid: String, resource: Drawable)
|
||||
|
||||
fun onVideoFileLoading(uid: String)
|
||||
fun onVideoFileLoadFailed(uid: String)
|
||||
fun onVideoFileReady(uid: String, file: File)
|
||||
}
|
||||
|
||||
internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget {
|
||||
override fun contextView(): ImageView = contextView
|
||||
|
||||
override fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.setImageDrawable(resource)
|
||||
}
|
||||
|
||||
override fun onVideoFileLoading(uid: String) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.isVisible = true
|
||||
holder.loaderProgressBar.isVisible = true
|
||||
holder.videoView.isVisible = false
|
||||
}
|
||||
|
||||
override fun onVideoFileLoadFailed(uid: String) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.videoFileLoadError()
|
||||
}
|
||||
|
||||
override fun onVideoFileReady(uid: String, file: File) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.isVisible = false
|
||||
holder.loaderProgressBar.isVisible = false
|
||||
holder.videoView.isVisible = true
|
||||
holder.videoReady(file)
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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.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
|
||||
private var progress: Int = 0
|
||||
private var wasPaused = false
|
||||
|
||||
var eventListener: WeakReference<AttachmentEventListener>? = null
|
||||
|
||||
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)
|
||||
|
||||
internal val target = DefaultVideoLoaderTarget(this, thumbnailImage)
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
mVideoPath = null
|
||||
}
|
||||
|
||||
fun videoReady(file: File) {
|
||||
mVideoPath = file.path
|
||||
if (isSelected) {
|
||||
startPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
fun videoFileLoadError() {
|
||||
}
|
||||
|
||||
override fun entersBackground() {
|
||||
if (videoView.isPlaying) {
|
||||
progress = videoView.currentPosition
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
videoView.stopPlayback()
|
||||
videoView.pause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun entersForeground() {
|
||||
onSelected(isSelected)
|
||||
}
|
||||
|
||||
override fun onSelected(selected: Boolean) {
|
||||
if (!selected) {
|
||||
if (videoView.isPlaying) {
|
||||
progress = videoView.currentPosition
|
||||
videoView.stopPlayback()
|
||||
} else {
|
||||
progress = 0
|
||||
}
|
||||
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)
|
||||
if (!wasPaused) {
|
||||
videoView.start()
|
||||
if (progress > 0) {
|
||||
videoView.seekTo(progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleCommand(commands: AttachmentCommands) {
|
||||
if (!isSelected) return
|
||||
when (commands) {
|
||||
AttachmentCommands.StartVideo -> {
|
||||
wasPaused = false
|
||||
videoView.start()
|
||||
}
|
||||
AttachmentCommands.PauseVideo -> {
|
||||
wasPaused = true
|
||||
videoView.pause()
|
||||
}
|
||||
is AttachmentCommands.SeekTo -> {
|
||||
val duration = videoView.duration
|
||||
if (duration > 0) {
|
||||
val seekDuration = duration * (commands.percentProgress / 100f)
|
||||
videoView.seekTo(seekDuration.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(attachmentInfo: AttachmentInfo) {
|
||||
super.bind(attachmentInfo)
|
||||
progress = 0
|
||||
wasPaused = false
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.view.View
|
||||
import android.widget.ProgressBar
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
|
||||
class ZoomableImageViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView) {
|
||||
|
||||
val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView)
|
||||
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
|
||||
|
||||
init {
|
||||
touchImageView.setAllowParentInterceptOnEdge(false)
|
||||
touchImageView.setOnScaleChangeListener { scaleFactor, _, _ ->
|
||||
// Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor")
|
||||
// It's a bit annoying but when you pitch down the scaling
|
||||
// is not exactly one :/
|
||||
touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f)
|
||||
}
|
||||
touchImageView.setScale(1.0f, true)
|
||||
touchImageView.setAllowParentInterceptOnEdge(true)
|
||||
}
|
||||
|
||||
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView)
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/rootContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".AttachmentViewerActivity">
|
||||
|
||||
<View
|
||||
android:id="@+id/backgroundView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="1"
|
||||
android:background="@android:color/black" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/dismissContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/transitionImageContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="UselessParent"
|
||||
tools:visibility="invisible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/transitionImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/attachmentPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
@ -0,0 +1,22 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/imageLoaderProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:visibility="visible"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
@ -0,0 +1,22 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/touchImageView"
|
||||
android:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/imageLoaderProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:visibility="visible"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
@ -0,0 +1,49 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoThumbnailImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:scaleType="centerInside" />
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
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>
|
@ -0,0 +1,17 @@
|
||||
<?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:background="@color/design_default_color_primary">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/testPage"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1"
|
||||
android:textSize="80sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
@ -39,6 +39,8 @@ allprojects {
|
||||
includeGroupByRegex "com\\.github\\.yalantis"
|
||||
// JsonViewer
|
||||
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
||||
// PhotoView
|
||||
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||
}
|
||||
}
|
||||
maven {
|
||||
|
@ -39,4 +39,6 @@ interface TimelineService {
|
||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
|
||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||
|
||||
fun getAttachmentMessages() : List<TimelineEvent>
|
||||
}
|
||||
|
@ -21,19 +21,25 @@ import androidx.lifecycle.Transformations
|
||||
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
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.crypto.store.db.doWithRealm
|
||||
import im.vector.matrix.android.internal.database.mapper.ReadReceiptsSummaryMapper
|
||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.fetchCopyMap
|
||||
import io.realm.Sort
|
||||
import io.realm.kotlin.where
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||
@ -88,4 +94,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||
events.firstOrNull().toOptional()
|
||||
}
|
||||
}
|
||||
|
||||
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() || it.root.isVideoMessage() } }
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,6 @@
|
||||
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'
|
||||
include ':vector'
|
||||
include ':matrix-sdk-android'
|
||||
include ':matrix-sdk-android-rx'
|
||||
include ':diff-match-patch'
|
||||
include ':attachment-viewer'
|
||||
include ':multipicker'
|
||||
|
@ -279,6 +279,7 @@ dependencies {
|
||||
implementation project(":matrix-sdk-android-rx")
|
||||
implementation project(":diff-match-patch")
|
||||
implementation project(":multipicker")
|
||||
implementation project(":attachment-viewer")
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
@ -368,6 +369,10 @@ dependencies {
|
||||
implementation "com.github.piasy:GlideImageLoader:$big_image_viewer_version"
|
||||
implementation "com.github.piasy:ProgressPieIndicator:$big_image_viewer_version"
|
||||
implementation "com.github.piasy:GlideImageViewFactory:$big_image_viewer_version"
|
||||
|
||||
// implementation 'com.github.MikeOrtiz:TouchImageView:3.0.2'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
||||
|
||||
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||
implementation 'com.danikula:videocache:2.7.1'
|
||||
|
@ -85,6 +85,11 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name=".features.media.ImageMediaViewerActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".features.media.VectorAttachmentViewerActivity"
|
||||
android:theme="@style/AppTheme.Transparent" />
|
||||
|
||||
<activity android:name=".features.media.BigImageViewerActivity" />
|
||||
<activity
|
||||
android:name=".features.rageshake.BugReportActivity"
|
||||
|
@ -389,6 +389,9 @@ SOFTWARE.
|
||||
<li>
|
||||
<b>BillCarsonFr/JsonViewer</b>
|
||||
</li>
|
||||
<li>
|
||||
<b>Copyright (C) 2018 stfalcon.com</b>
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
Apache License
|
||||
|
@ -48,6 +48,7 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity
|
||||
import im.vector.riotx.features.invite.VectorInviteView
|
||||
import im.vector.riotx.features.link.LinkHandlerActivity
|
||||
import im.vector.riotx.features.login.LoginActivity
|
||||
import im.vector.riotx.features.media.VectorAttachmentViewerActivity
|
||||
import im.vector.riotx.features.media.BigImageViewerActivity
|
||||
import im.vector.riotx.features.media.ImageMediaViewerActivity
|
||||
import im.vector.riotx.features.media.VideoMediaViewerActivity
|
||||
@ -135,6 +136,7 @@ interface ScreenComponent {
|
||||
fun inject(activity: ReviewTermsActivity)
|
||||
fun inject(activity: WidgetActivity)
|
||||
fun inject(activity: VectorCallActivity)
|
||||
fun inject(activity: VectorAttachmentViewerActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
@ -1171,14 +1171,27 @@ class RoomDetailFragment @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
|
||||
navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
|
||||
navigator.openMediaViewer(
|
||||
activity = requireActivity(),
|
||||
roomId = roomDetailArgs.roomId,
|
||||
mediaData = mediaData,
|
||||
view = view
|
||||
) { pairs ->
|
||||
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
|
||||
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
|
||||
navigator.openVideoViewer(requireActivity(), mediaData)
|
||||
navigator.openMediaViewer(
|
||||
activity = requireActivity(),
|
||||
roomId = roomDetailArgs.roomId,
|
||||
mediaData = mediaData,
|
||||
view = view
|
||||
) { pairs ->
|
||||
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
|
||||
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
|
||||
}
|
||||
}
|
||||
|
||||
// override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {
|
||||
|
@ -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,
|
||||
|
@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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.features.media
|
||||
|
||||
import android.content.Context
|
||||
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), AttachmentEventListener {
|
||||
|
||||
var onShareCallback: (() -> Unit)? = null
|
||||
var onBack: (() -> Unit)? = null
|
||||
var onPlayPause: ((play: Boolean) -> Unit)? = null
|
||||
var videoSeekTo: ((progress: Int) -> Unit)? = null
|
||||
|
||||
private val counterTextView: TextView
|
||||
private val infoTextView: TextView
|
||||
private val shareImage: ImageView
|
||||
private val overlayPlayPauseButton: ImageView
|
||||
private val overlaySeekBar: SeekBar
|
||||
|
||||
var isPlaying = false
|
||||
|
||||
val videoControlsGroup: Group
|
||||
|
||||
var suspendSeekBarUpdate = false
|
||||
|
||||
init {
|
||||
View.inflate(context, R.layout.merge_image_attachment_overlay, this)
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
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)
|
||||
findViewById<ImageView>(R.id.overlayBackButton).setOnClickListener {
|
||||
onBack?.invoke()
|
||||
}
|
||||
findViewById<ImageView>(R.id.overlayShareButton).setOnClickListener {
|
||||
onShareCallback?.invoke()
|
||||
}
|
||||
findViewById<ImageView>(R.id.overlayPlayPauseButton).setOnClickListener {
|
||||
onPlayPause?.invoke(!isPlaying)
|
||||
}
|
||||
|
||||
overlaySeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
videoSeekTo?.invoke(progress)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||
suspendSeekBarUpdate = true
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
suspendSeekBarUpdate = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
if (!suspendSeekBarUpdate) {
|
||||
val safeDuration = (if (event.duration == 0) 100 else event.duration).toFloat()
|
||||
val percent = ((event.progress / safeDuration) * 100f).toInt().coerceAtMost(100)
|
||||
isPlaying = event.isPlaying
|
||||
overlaySeekBar.progress = percent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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.features.media
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
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.file.FileService
|
||||
import im.vector.riotx.attachmentviewer.AttachmentInfo
|
||||
import im.vector.riotx.attachmentviewer.AttachmentSourceProvider
|
||||
import im.vector.riotx.attachmentviewer.ImageLoaderTarget
|
||||
import im.vector.riotx.attachmentviewer.VideoLoaderTarget
|
||||
import java.io.File
|
||||
|
||||
abstract class BaseAttachmentProvider(val imageContentRenderer: ImageContentRenderer, val fileService: FileService) : AttachmentSourceProvider {
|
||||
|
||||
interface InteractionListener {
|
||||
fun onDismissTapped()
|
||||
fun onShareTapped()
|
||||
fun onPlayPause(play: Boolean)
|
||||
fun videoSeekTo(percent: Int)
|
||||
}
|
||||
|
||||
var interactionListener: InteractionListener? = null
|
||||
|
||||
protected var overlayView: AttachmentOverlayView? = null
|
||||
|
||||
override fun overlayViewAtPosition(context: Context, position: Int): View? {
|
||||
if (position == -1) return null
|
||||
if (overlayView == null) {
|
||||
overlayView = AttachmentOverlayView(context)
|
||||
overlayView?.onBack = {
|
||||
interactionListener?.onDismissTapped()
|
||||
}
|
||||
overlayView?.onShareCallback = {
|
||||
interactionListener?.onShareTapped()
|
||||
}
|
||||
overlayView?.onPlayPause = { play ->
|
||||
interactionListener?.onPlayPause(play)
|
||||
}
|
||||
overlayView?.videoSeekTo = { percent ->
|
||||
interactionListener?.videoSeekTo(percent)
|
||||
}
|
||||
}
|
||||
return overlayView
|
||||
}
|
||||
|
||||
override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image) {
|
||||
(info.data as? ImageContentRenderer.Data)?.let {
|
||||
imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget<ImageView, Drawable>(target.contextView()) {
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
target.onLoadFailed(info.uid, errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(placeholder: Drawable?) {
|
||||
target.onResourceCleared(info.uid, placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
target.onResourceReady(info.uid, resource)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage) {
|
||||
(info.data as? ImageContentRenderer.Data)?.let {
|
||||
imageContentRenderer.render(it, target.contextView(), object : CustomViewTarget<ImageView, Drawable>(target.contextView()) {
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
target.onLoadFailed(info.uid, errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(placeholder: Drawable?) {
|
||||
target.onResourceCleared(info.uid, placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
target.onResourceReady(info.uid, resource)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadVideo(target: VideoLoaderTarget, 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, target.contextView(), object : CustomViewTarget<ImageView, Drawable>(target.contextView()) {
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
target.onThumbnailLoadFailed(info.uid, errorDrawable)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(placeholder: Drawable?) {
|
||||
target.onThumbnailResourceCleared(info.uid, placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
target.onThumbnailResourceReady(info.uid, resource)
|
||||
}
|
||||
})
|
||||
|
||||
target.onVideoFileLoading(info.uid)
|
||||
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) {
|
||||
target.onVideoFileReady(info.uid, data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
target.onVideoFileLoadFailed(info.uid)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun clear(id: String) {
|
||||
// TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
abstract fun getFileForSharing(position: Int, callback: ((File?) -> Unit))
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.features.media
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.isVideoMessage
|
||||
import im.vector.matrix.android.api.session.file.FileService
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.riotx.attachmentviewer.AttachmentInfo
|
||||
import im.vector.riotx.core.date.VectorDateFormatter
|
||||
import im.vector.riotx.core.extensions.localDateTime
|
||||
import java.io.File
|
||||
|
||||
class DataAttachmentRoomProvider(
|
||||
private val attachments: List<AttachmentData>,
|
||||
private val room: Room?,
|
||||
private val initialIndex: Int,
|
||||
imageContentRenderer: ImageContentRenderer,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
fileService: FileService) : BaseAttachmentProvider(imageContentRenderer, fileService) {
|
||||
|
||||
override fun getItemCount(): Int = attachments.size
|
||||
|
||||
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
|
||||
return attachments[position].let {
|
||||
when (it) {
|
||||
is ImageContentRenderer.Data -> {
|
||||
if (it.mimeType == "image/gif") {
|
||||
AttachmentInfo.AnimatedImage(
|
||||
uid = it.eventId,
|
||||
url = it.url ?: "",
|
||||
data = it
|
||||
)
|
||||
} else {
|
||||
AttachmentInfo.Image(
|
||||
uid = it.eventId,
|
||||
url = it.url ?: "",
|
||||
data = it
|
||||
)
|
||||
}
|
||||
}
|
||||
is VideoContentRenderer.Data -> {
|
||||
AttachmentInfo.Video(
|
||||
uid = it.eventId,
|
||||
url = it.url ?: "",
|
||||
data = it,
|
||||
thumbnail = AttachmentInfo.Image(
|
||||
uid = it.eventId,
|
||||
url = it.thumbnailMediaData.url ?: "",
|
||||
data = it.thumbnailMediaData
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun overlayViewAtPosition(context: Context, position: Int): View? {
|
||||
super.overlayViewAtPosition(context, position)
|
||||
val item = attachments[position]
|
||||
val timeLineEvent = room?.getTimeLineEvent(item.eventId)
|
||||
if (timeLineEvent != null) {
|
||||
val dateString = timeLineEvent.root.localDateTime().let {
|
||||
"${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
|
||||
}
|
||||
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString")
|
||||
overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage()
|
||||
} else {
|
||||
overlayView?.updateWith("", "")
|
||||
}
|
||||
return overlayView
|
||||
}
|
||||
|
||||
override fun getFileForSharing(position: Int, callback: (File?) -> Unit) {
|
||||
val item = attachments[position]
|
||||
fileService.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = item.eventId,
|
||||
fileName = item.filename,
|
||||
mimeType = item.mimeType,
|
||||
url = item.url ?: "",
|
||||
elementToDecrypt = item.elementToDecrypt,
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
callback(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -19,11 +19,13 @@ package im.vector.riotx.features.media
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.CustomViewTarget
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
|
||||
import com.github.piasy.biv.view.BigImageView
|
||||
@ -42,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()
|
||||
}
|
||||
@ -93,6 +103,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
fun render(data: Data, contextView: View, target: CustomViewTarget<*, Drawable>) {
|
||||
val req = if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
GlideApp
|
||||
.with(contextView)
|
||||
.load(data)
|
||||
} else {
|
||||
// Clear image
|
||||
val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
|
||||
GlideApp
|
||||
.with(contextView)
|
||||
.load(resolvedUrl)
|
||||
}
|
||||
|
||||
req.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
|
||||
.fitCenter()
|
||||
.into(target)
|
||||
}
|
||||
|
||||
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
|
||||
val size = processSize(data, mode)
|
||||
|
||||
@ -122,6 +151,45 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
fun renderThumbnailDontTransform(data: Data, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
|
||||
// a11y
|
||||
imageView.contentDescription = data.filename
|
||||
|
||||
val req = if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(data)
|
||||
} else {
|
||||
// Clear image
|
||||
val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver().resolveFullSize(data.url)
|
||||
GlideApp
|
||||
.with(imageView)
|
||||
.load(resolvedUrl)
|
||||
}
|
||||
|
||||
req.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
isFirstResource: Boolean): Boolean {
|
||||
callback?.invoke(false)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource?,
|
||||
isFirstResource: Boolean): Boolean {
|
||||
callback?.invoke(true)
|
||||
return false
|
||||
}
|
||||
})
|
||||
.dontTransform()
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||
return if (data.elementToDecrypt != null) {
|
||||
// Encrypted image
|
||||
|
@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
|
||||
encryptedImageView.isVisible = false
|
||||
// Postpone transaction a bit until thumbnail is loaded
|
||||
supportPostponeEnterTransition()
|
||||
|
||||
// We are not passing the exact same image that in the
|
||||
imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) {
|
||||
// Proceed with transaction
|
||||
scheduleStartPostponedTransition(imageTransitionView)
|
||||
|
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* 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.features.media
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.view.isVisible
|
||||
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.Room
|
||||
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
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.riotx.attachmentviewer.AttachmentInfo
|
||||
import im.vector.riotx.core.date.VectorDateFormatter
|
||||
import im.vector.riotx.core.extensions.localDateTime
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomEventsAttachmentProvider(
|
||||
private val attachments: List<TimelineEvent>,
|
||||
private val initialIndex: Int,
|
||||
imageContentRenderer: ImageContentRenderer,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
fileService: FileService
|
||||
) : BaseAttachmentProvider(imageContentRenderer, fileService) {
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return attachments.size
|
||||
}
|
||||
|
||||
override fun getAttachmentInfoAt(position: Int): AttachmentInfo {
|
||||
return attachments[position].let {
|
||||
val content = it.root.getClearContent().toModel<MessageContent>() as? MessageWithAttachmentContent
|
||||
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(
|
||||
uid = it.eventId,
|
||||
url = content.url ?: "",
|
||||
data = data
|
||||
)
|
||||
} else {
|
||||
AttachmentInfo.Image(
|
||||
uid = it.eventId,
|
||||
url = content.url ?: "",
|
||||
data = 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(
|
||||
uid = it.eventId,
|
||||
url = content.getFileUrl() ?: "",
|
||||
data = data,
|
||||
thumbnail = AttachmentInfo.Image(
|
||||
uid = it.eventId,
|
||||
url = content.videoInfo?.thumbnailFile?.url
|
||||
?: content.videoInfo?.thumbnailUrl ?: "",
|
||||
data = thumbnailData
|
||||
|
||||
)
|
||||
)
|
||||
} else {
|
||||
AttachmentInfo.Image(
|
||||
uid = it.eventId,
|
||||
url = "",
|
||||
data = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun overlayViewAtPosition(context: Context, position: Int): View? {
|
||||
super.overlayViewAtPosition(context, position)
|
||||
val item = attachments[position]
|
||||
val dateString = item.root.localDateTime().let {
|
||||
"${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 getFileForSharing(position: Int, callback: (File?) -> Unit) {
|
||||
attachments[position].let { timelineEvent ->
|
||||
|
||||
val messageContent = timelineEvent.root.getClearContent().toModel<MessageContent>()
|
||||
as? MessageWithAttachmentContent
|
||||
?: return@let
|
||||
fileService.downloadFile(
|
||||
downloadMode = FileService.DownloadMode.FOR_EXTERNAL_SHARE,
|
||||
id = timelineEvent.eventId,
|
||||
fileName = messageContent.body,
|
||||
mimeType = messageContent.mimeType,
|
||||
url = messageContent.getFileUrl(),
|
||||
elementToDecrypt = messageContent.encryptedFileInfo?.toElementToDecrypt(),
|
||||
callback = object : MatrixCallback<File> {
|
||||
override fun onSuccess(data: File) {
|
||||
callback(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback(null)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AttachmentProviderFactory @Inject constructor(
|
||||
private val imageContentRenderer: ImageContentRenderer,
|
||||
private val vectorDateFormatter: VectorDateFormatter,
|
||||
private val session: Session
|
||||
) {
|
||||
|
||||
fun createProvider(attachments: List<TimelineEvent>, initialIndex: Int): RoomEventsAttachmentProvider {
|
||||
return RoomEventsAttachmentProvider(attachments, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
|
||||
}
|
||||
|
||||
fun createProvider(attachments: List<AttachmentData>, room: Room?, initialIndex: Int): DataAttachmentRoomProvider {
|
||||
return DataAttachmentRoomProvider(attachments, room, initialIndex, imageContentRenderer, vectorDateFormatter, session.fileService())
|
||||
}
|
||||
}
|
@ -0,0 +1,277 @@
|
||||
/*
|
||||
* 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.features.media
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.transition.addListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.transition.Transition
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.attachmentviewer.AttachmentCommands
|
||||
import im.vector.riotx.attachmentviewer.AttachmentViewerActivity
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.DaggerScreenComponent
|
||||
import im.vector.riotx.core.di.HasVectorInjector
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.di.VectorComponent
|
||||
import im.vector.riotx.core.intent.getMimeTypeFromUri
|
||||
import im.vector.riotx.core.utils.shareMedia
|
||||
import im.vector.riotx.features.themes.ActivityOtherThemes
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
class VectorAttachmentViewerActivity : AttachmentViewerActivity(), BaseAttachmentProvider.InteractionListener {
|
||||
|
||||
@Parcelize
|
||||
data class Args(
|
||||
val roomId: String?,
|
||||
val eventId: String,
|
||||
val sharedTransitionName: String?
|
||||
) : Parcelable
|
||||
|
||||
@Inject
|
||||
lateinit var sessionHolder: ActiveSessionHolder
|
||||
|
||||
@Inject
|
||||
lateinit var dataSourceFactory: AttachmentProviderFactory
|
||||
|
||||
@Inject
|
||||
lateinit var imageContentRenderer: ImageContentRenderer
|
||||
|
||||
private lateinit var screenComponent: ScreenComponent
|
||||
|
||||
private var initialIndex = 0
|
||||
private var isAnimatingOut = false
|
||||
|
||||
var currentSourceProvider: BaseAttachmentProvider? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Timber.i("onCreate Activity ${this.javaClass.simpleName}")
|
||||
val vectorComponent = getVectorComponent()
|
||||
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
|
||||
val timeForInjection = measureTimeMillis {
|
||||
screenComponent.inject(this)
|
||||
}
|
||||
Timber.v("Injecting dependencies into ${javaClass.simpleName} took $timeForInjection ms")
|
||||
ThemeUtils.setActivityTheme(this, getOtherThemes())
|
||||
|
||||
val args = args() ?: throw IllegalArgumentException("Missing arguments")
|
||||
|
||||
if (savedInstanceState == null && addTransitionListener()) {
|
||||
args.sharedTransitionName?.let {
|
||||
ViewCompat.setTransitionName(imageTransitionView, it)
|
||||
transitionImageContainer.isVisible = true
|
||||
|
||||
// Postpone transaction a bit until thumbnail is loaded
|
||||
val mediaData: Parcelable? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
|
||||
if (mediaData is ImageContentRenderer.Data) {
|
||||
// will be shown at end of transition
|
||||
pager2.isInvisible = true
|
||||
supportPostponeEnterTransition()
|
||||
imageContentRenderer.renderThumbnailDontTransform(mediaData, imageTransitionView) {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
|
||||
|
||||
val room = args.roomId?.let { session.getRoom(it) }
|
||||
|
||||
val inMemoryData = intent.getParcelableArrayListExtra<AttachmentData>(EXTRA_IN_MEMORY_DATA)
|
||||
if (inMemoryData != null) {
|
||||
val sourceProvider = dataSourceFactory.createProvider(inMemoryData, room, initialIndex)
|
||||
val index = inMemoryData.indexOfFirst { it.eventId == args.eventId }
|
||||
initialIndex = index
|
||||
sourceProvider.interactionListener = this
|
||||
setSourceProvider(sourceProvider)
|
||||
this.currentSourceProvider = sourceProvider
|
||||
if (savedInstanceState == null) {
|
||||
pager2.setCurrentItem(index, false)
|
||||
// The page change listener is not notified of the change...
|
||||
pager2.post {
|
||||
onSelectedPositionChanged(index)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val events = room?.getAttachmentMessages()
|
||||
?: emptyList()
|
||||
val index = events.indexOfFirst { it.eventId == args.eventId }
|
||||
initialIndex = index
|
||||
|
||||
val sourceProvider = dataSourceFactory.createProvider(events, index)
|
||||
sourceProvider.interactionListener = this
|
||||
setSourceProvider(sourceProvider)
|
||||
this.currentSourceProvider = 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)
|
||||
window.navigationBarColor = ContextCompat.getColor(this, R.color.black_alpha)
|
||||
}
|
||||
|
||||
private fun getOtherThemes() = ActivityOtherThemes.VectorAttachmentsPreview
|
||||
|
||||
override fun shouldAnimateDismiss(): Boolean {
|
||||
return currentPosition != initialIndex
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (currentPosition == initialIndex) {
|
||||
// show back the transition view
|
||||
// TODO, we should track and update the mapping
|
||||
transitionImageContainer.isVisible = true
|
||||
}
|
||||
isAnimatingOut = true
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun animateClose() {
|
||||
if (currentPosition == initialIndex) {
|
||||
// show back the transition view
|
||||
// TODO, we should track and update the mapping
|
||||
transitionImageContainer.isVisible = true
|
||||
}
|
||||
isAnimatingOut = true
|
||||
ActivityCompat.finishAfterTransition(this)
|
||||
}
|
||||
|
||||
// ==========================================================================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================================================================
|
||||
|
||||
/**
|
||||
* Try and add a [Transition.TransitionListener] to the entering shared element
|
||||
* [Transition]. We do this so that we can load the full-size image after the transition
|
||||
* has completed.
|
||||
*
|
||||
* @return true if we were successful in adding a listener to the enter transition
|
||||
*/
|
||||
private fun addTransitionListener(): Boolean {
|
||||
val transition = window.sharedElementEnterTransition
|
||||
|
||||
if (transition != null) {
|
||||
// There is an entering shared element transition so add a listener to it
|
||||
transition.addListener(
|
||||
onEnd = {
|
||||
// The listener is also called when we are exiting
|
||||
// so we use a boolean to avoid reshowing pager at end of dismiss transition
|
||||
if (!isAnimatingOut) {
|
||||
transitionImageContainer.isVisible = false
|
||||
pager2.isInvisible = false
|
||||
}
|
||||
},
|
||||
onCancel = {
|
||||
if (!isAnimatingOut) {
|
||||
transitionImageContainer.isVisible = false
|
||||
pager2.isInvisible = false
|
||||
}
|
||||
}
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
// If we reach here then we have not added a listener
|
||||
return false
|
||||
}
|
||||
|
||||
private fun args() = intent.getParcelableExtra<Args>(EXTRA_ARGS)
|
||||
|
||||
private fun getVectorComponent(): VectorComponent {
|
||||
return (application as HasVectorInjector).injector()
|
||||
}
|
||||
|
||||
private fun scheduleStartPostponedTransition(sharedElement: View) {
|
||||
sharedElement.viewTreeObserver.addOnPreDrawListener(
|
||||
object : ViewTreeObserver.OnPreDrawListener {
|
||||
override fun onPreDraw(): Boolean {
|
||||
sharedElement.viewTreeObserver.removeOnPreDrawListener(this)
|
||||
supportStartPostponedEnterTransition()
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_ARGS = "EXTRA_ARGS"
|
||||
const val EXTRA_IMAGE_DATA = "EXTRA_IMAGE_DATA"
|
||||
const val EXTRA_IN_MEMORY_DATA = "EXTRA_IN_MEMORY_DATA"
|
||||
|
||||
fun newIntent(context: Context,
|
||||
mediaData: AttachmentData,
|
||||
roomId: String?,
|
||||
eventId: String,
|
||||
inMemoryData: List<AttachmentData>,
|
||||
sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {
|
||||
it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName))
|
||||
it.putExtra(EXTRA_IMAGE_DATA, mediaData)
|
||||
if (inMemoryData.isNotEmpty()) {
|
||||
it.putParcelableArrayListExtra(EXTRA_IN_MEMORY_DATA, ArrayList(inMemoryData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDismissTapped() {
|
||||
animateClose()
|
||||
}
|
||||
|
||||
override fun onPlayPause(play: Boolean) {
|
||||
handle(if (play) AttachmentCommands.StartVideo else AttachmentCommands.PauseVideo)
|
||||
}
|
||||
|
||||
override fun videoSeekTo(percent: Int) {
|
||||
handle(AttachmentCommands.SeekTo(percent))
|
||||
}
|
||||
|
||||
override fun onShareTapped() {
|
||||
this.currentSourceProvider?.getFileForSharing(currentPosition) { data ->
|
||||
if (data != null && lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
shareMedia(this@VectorAttachmentViewerActivity, data, getMimeTypeFromUri(this@VectorAttachmentViewerActivity, data.toUri()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -19,7 +19,6 @@ package im.vector.riotx.features.navigation
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
@ -49,11 +48,9 @@ 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.ImageMediaViewerActivity
|
||||
import im.vector.riotx.features.media.VideoContentRenderer
|
||||
import im.vector.riotx.features.media.VideoMediaViewerActivity
|
||||
import im.vector.riotx.features.media.VectorAttachmentViewerActivity
|
||||
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
||||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
|
||||
@ -89,7 +86,8 @@ class DefaultNavigator @Inject constructor(
|
||||
|
||||
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
|
||||
val session = sessionHolder.getSafeActiveSession() ?: return
|
||||
val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId) ?: return
|
||||
val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId)
|
||||
?: return
|
||||
(tx as? IncomingSasVerificationTransaction)?.performAccept()
|
||||
if (context is VectorBaseActivity) {
|
||||
VerificationBottomSheet.withArgs(
|
||||
@ -237,7 +235,8 @@ class DefaultNavigator @Inject constructor(
|
||||
?.let { avatarUrl ->
|
||||
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
|
||||
val options = sharedElement?.let {
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
|
||||
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
|
||||
?: "")
|
||||
}
|
||||
activity.startActivity(intent, options?.toBundle())
|
||||
}
|
||||
@ -265,27 +264,32 @@ class DefaultNavigator @Inject constructor(
|
||||
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
|
||||
}
|
||||
|
||||
override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?) {
|
||||
val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
|
||||
override fun openMediaViewer(activity: Activity,
|
||||
roomId: String,
|
||||
mediaData: AttachmentData,
|
||||
view: View,
|
||||
inMemory: List<AttachmentData>,
|
||||
options: ((MutableList<Pair<View, String>>) -> Unit)?) {
|
||||
VectorAttachmentViewerActivity.newIntent(activity,
|
||||
mediaData,
|
||||
roomId,
|
||||
mediaData.eventId,
|
||||
inMemory,
|
||||
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)
|
||||
}
|
||||
|
||||
override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) {
|
||||
val intent = VideoMediaViewerActivity.newIntent(activity, mediaData)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
|
||||
|
@ -23,11 +23,10 @@ import androidx.core.util.Pair
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||
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.matrix.android.api.util.MatrixItem
|
||||
import im.vector.riotx.features.home.room.detail.widget.WidgetRequestCodes
|
||||
import im.vector.riotx.features.media.ImageContentRenderer
|
||||
import im.vector.riotx.features.media.VideoContentRenderer
|
||||
import im.vector.riotx.features.media.AttachmentData
|
||||
import im.vector.riotx.features.settings.VectorSettingsActivity
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import im.vector.riotx.features.terms.ReviewTermsActivity
|
||||
@ -93,7 +92,10 @@ interface Navigator {
|
||||
|
||||
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
|
||||
|
||||
fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?)
|
||||
|
||||
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
|
||||
fun openMediaViewer(activity: Activity,
|
||||
roomId: String,
|
||||
mediaData: AttachmentData,
|
||||
view: View,
|
||||
inMemory: List<AttachmentData> = emptyList(),
|
||||
options: ((MutableList<Pair<View, String>>) -> Unit)?)
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import com.tapadoo.alerter.Alerter
|
||||
import com.tapadoo.alerter.OnHideAlertListener
|
||||
import dagger.Lazy
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import timber.log.Timber
|
||||
@ -83,7 +84,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||
setLightStatusBar()
|
||||
}
|
||||
}
|
||||
if (currentAlerter?.shouldBeDisplayedIn?.invoke(activity) == false) {
|
||||
if (currentAlerter?.shouldBeDisplayedIn?.invoke(activity) == false || activity !is VectorBaseActivity) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -20,23 +20,34 @@ import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.util.Pair
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
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.getFileUrl
|
||||
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.cleanup
|
||||
import im.vector.riotx.core.extensions.trackItemsVisibilityChange
|
||||
import im.vector.riotx.core.platform.StateView
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.features.media.AttachmentData
|
||||
import im.vector.riotx.features.media.ImageContentRenderer
|
||||
import im.vector.riotx.features.media.VideoContentRenderer
|
||||
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsAction
|
||||
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
|
||||
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewModel
|
||||
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsViewState
|
||||
import kotlinx.android.synthetic.main.fragment_generic_state_view_recycler.*
|
||||
import kotlinx.android.synthetic.main.fragment_room_uploads.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomUploadsMediaFragment @Inject constructor(
|
||||
@ -76,12 +87,86 @@ class RoomUploadsMediaFragment @Inject constructor(
|
||||
controller.listener = null
|
||||
}
|
||||
|
||||
override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) {
|
||||
navigator.openImageViewer(requireActivity(), mediaData, view, null)
|
||||
// It's very strange i can't just access
|
||||
// the app bar using find by id...
|
||||
private fun trickFindAppBar(): AppBarLayout? {
|
||||
return activity?.supportFragmentManager?.fragments
|
||||
?.filterIsInstance<RoomUploadsFragment>()
|
||||
?.firstOrNull()
|
||||
?.roomUploadsAppBar
|
||||
}
|
||||
|
||||
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
|
||||
navigator.openVideoViewer(requireActivity(), mediaData)
|
||||
override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) = withState(uploadsViewModel) { state ->
|
||||
val inMemory = getItemsArgs(state)
|
||||
navigator.openMediaViewer(
|
||||
activity = requireActivity(),
|
||||
roomId = state.roomId,
|
||||
mediaData = mediaData,
|
||||
view = view,
|
||||
inMemory = inMemory
|
||||
) { pairs ->
|
||||
trickFindAppBar()?.let {
|
||||
pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getItemsArgs(state: RoomUploadsViewState): List<AttachmentData> {
|
||||
return state.mediaEvents.mapNotNull {
|
||||
when (val content = it.contentWithAttachmentContent) {
|
||||
is MessageImageContent -> {
|
||||
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
|
||||
)
|
||||
}
|
||||
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
|
||||
)
|
||||
VideoContentRenderer.Data(
|
||||
eventId = it.eventId,
|
||||
filename = content.body,
|
||||
mimeType = content.mimeType,
|
||||
url = content.getFileUrl(),
|
||||
elementToDecrypt = content.encryptedFileInfo?.toElementToDecrypt(),
|
||||
thumbnailMediaData = thumbnailData
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) = withState(uploadsViewModel) { state ->
|
||||
val inMemory = getItemsArgs(state)
|
||||
navigator.openMediaViewer(
|
||||
activity = requireActivity(),
|
||||
roomId = state.roomId,
|
||||
mediaData = mediaData,
|
||||
view = view,
|
||||
inMemory = inMemory
|
||||
) { pairs ->
|
||||
trickFindAppBar()?.let {
|
||||
pairs.add(Pair(it, ViewCompat.getTransitionName(it) ?: ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadMore() {
|
||||
|
@ -18,11 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.media
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.media.ImageContentRenderer
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_uploads_image)
|
||||
@ -35,8 +37,13 @@ abstract class UploadsImageItem : VectorEpoxyModel<UploadsImageItem.Holder>() {
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
|
||||
holder.view.setOnClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { _ ->
|
||||
listener?.onItemClicked(holder.imageView, data)
|
||||
})
|
||||
)
|
||||
imageContentRenderer.render(data, holder.imageView, IMAGE_SIZE_DP)
|
||||
ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
|
@ -18,11 +18,13 @@ package im.vector.riotx.features.roomprofile.uploads.media
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||
import im.vector.riotx.features.media.ImageContentRenderer
|
||||
import im.vector.riotx.features.media.VideoContentRenderer
|
||||
|
||||
@ -36,8 +38,13 @@ abstract class UploadsVideoItem : VectorEpoxyModel<UploadsVideoItem.Holder>() {
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.view.setOnClickListener { listener?.onItemClicked(holder.imageView, data) }
|
||||
holder.view.setOnClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { _ ->
|
||||
listener?.onItemClicked(holder.imageView, data)
|
||||
})
|
||||
)
|
||||
imageContentRenderer.render(data.thumbnailMediaData, holder.imageView, IMAGE_SIZE_DP)
|
||||
ViewCompat.setTransitionName(holder.imageView, "videoPreview_${id()}")
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
|
@ -38,4 +38,10 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int,
|
||||
R.style.AppTheme_AttachmentsPreview,
|
||||
R.style.AppTheme_AttachmentsPreview
|
||||
)
|
||||
|
||||
object VectorAttachmentsPreview : ActivityOtherThemes(
|
||||
R.style.AppTheme_Transparent,
|
||||
R.style.AppTheme_Transparent,
|
||||
R.style.AppTheme_Transparent
|
||||
)
|
||||
}
|
||||
|
10
vector/src/main/res/drawable/ic_pause.xml
Normal file
10
vector/src/main/res/drawable/ic_pause.xml
Normal 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>
|
10
vector/src/main/res/drawable/ic_play_arrow.xml
Normal file
10
vector/src/main/res/drawable/ic_play_arrow.xml
Normal 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>
|
@ -8,6 +8,8 @@
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
style="@style/VectorAppBarLayoutStyle"
|
||||
android:id="@+id/roomUploadsAppBar"
|
||||
android:transitionName="toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="4dp">
|
||||
|
133
vector/src/main/res/layout/merge_image_attachment_overlay.xml
Normal file
133
vector/src/main/res/layout/merge_image_attachment_overlay.xml
Normal file
@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge 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:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
|
||||
|
||||
<View
|
||||
android:id="@+id/overlayTopBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:background="@color/black_alpha"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
</View>
|
||||
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/overlayBackButton"
|
||||
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/action_close"
|
||||
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"
|
||||
app:layout_constraintTop_toTopOf="@id/overlayTopBackground"
|
||||
app:srcCompat="@drawable/ic_back_24dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/overlayCounterText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@id/overlayInfoText"
|
||||
app:layout_constraintEnd_toStartOf="@id/overlayShareButton"
|
||||
app:layout_constraintStart_toEndOf="@id/overlayBackButton"
|
||||
app:layout_constraintTop_toTopOf="@id/overlayTopBackground"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="1 of 200" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/overlayInfoText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground"
|
||||
app:layout_constraintEnd_toStartOf="@id/overlayShareButton"
|
||||
app:layout_constraintStart_toStartOf="@id/overlayCounterText"
|
||||
app:layout_constraintTop_toBottomOf="@id/overlayCounterText"
|
||||
tools:text="Bill 29 Jun at 19:42" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/overlayShareButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/share"
|
||||
android:focusable="true"
|
||||
android:padding="6dp"
|
||||
android:tint="@color/white"
|
||||
app:layout_constraintBottom_toBottomOf="@id/overlayTopBackground"
|
||||
app:layout_constraintEnd_toEndOf="@id/overlayTopBackground"
|
||||
app:layout_constraintTop_toTopOf="@id/overlayTopBackground"
|
||||
app:srcCompat="@drawable/ic_share" />
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/overlayVideoControlsGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="overlayBottomBackground,overlayBackButton,overlayPlayPauseButton,overlaySeekBar"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<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:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:backgroundTint="@color/white"
|
||||
android:progressBackgroundTint="@color/white"
|
||||
android:thumbTint="@color/white"
|
||||
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>
|
@ -40,6 +40,7 @@
|
||||
<!-- Other usefull color -->
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="black_alpha">#55000000</color>
|
||||
|
||||
<!-- Palette: format fo naming:
|
||||
'riotx_<name in the palette snake case>_<theme>'
|
||||
|
@ -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>
|
||||
|
@ -10,4 +10,15 @@
|
||||
|
||||
<style name="AppTheme.AttachmentsPreview" parent="AppTheme.Base.Black"/>
|
||||
|
||||
<style name="AppTheme.Transparent" parent="AppTheme.Base.Black">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowContentOverlay">@null</item>
|
||||
<item name="android:windowNoTitle">true</item>
|
||||
<!-- <item name="android:windowIsFloating">true</item>-->
|
||||
<item name="android:backgroundDimEnabled">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user