mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-01-30 11:43:46 +03:00
Initial commit
This commit is contained in:
parent
f1e5129acb
commit
4a2a6d34ae
37 changed files with 1409 additions and 31 deletions
1
attachment-viewer/.gitignore
vendored
Normal file
1
attachment-viewer/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
81
attachment-viewer/build.gradle
Normal file
81
attachment-viewer/build.gradle
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
buildToolsVersion "29.0.3"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion 21
|
||||||
|
targetSdkVersion 29
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
consumerProguardFiles "consumer-rules.pro"
|
||||||
|
}
|
||||||
|
|
||||||
|
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.MikeOrtiz:TouchImageView:3.0.2'
|
||||||
|
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
||||||
|
implementation "com.github.bumptech.glide:glide:4.10.0"
|
||||||
|
|
||||||
|
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
|
11
attachment-viewer/src/main/AndroidManifest.xml
Normal file
11
attachment-viewer/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="im.vector.riotx.attachment_viewer">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="im.vector.riotx.attachment_viewer.AttachmentViewerActivity"
|
||||||
|
android:theme="@style/Theme.Transparent" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* 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.attachment_viewer
|
||||||
|
|
||||||
|
sealed class AttachmentInfo {
|
||||||
|
data class Image(val url: String, val data: Any?) : AttachmentInfo()
|
||||||
|
data class Video(val url: String, val data: Any) : AttachmentInfo()
|
||||||
|
data class Audio(val url: String, val data: Any) : AttachmentInfo()
|
||||||
|
data class File(val url: String, val data: Any) : AttachmentInfo()
|
||||||
|
|
||||||
|
fun bind() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttachmentSourceProvider {
|
||||||
|
|
||||||
|
fun getItemCount(): Int
|
||||||
|
|
||||||
|
fun getAttachmentInfoAt(position: Int): AttachmentInfo
|
||||||
|
|
||||||
|
fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image)
|
||||||
|
}
|
|
@ -0,0 +1,210 @@
|
||||||
|
/*
|
||||||
|
* 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.attachment_viewer
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.ScaleGestureDetector
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import kotlinx.android.synthetic.main.activity_attachment_viewer.*
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
abstract class AttachmentViewerActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
lateinit var pager2: ViewPager2
|
||||||
|
lateinit var imageTransitionView: ImageView
|
||||||
|
lateinit var transitionImageContainer: ViewGroup
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
private var overlayView: View? = null
|
||||||
|
|
||||||
|
private lateinit var swipeDismissHandler: SwipeToDismissHandler
|
||||||
|
private lateinit var directionDetector: SwipeDirectionDetector
|
||||||
|
private lateinit var scaleDetector: ScaleGestureDetector
|
||||||
|
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
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()
|
||||||
|
|
||||||
|
attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||||
|
override fun onPageScrollStateChanged(state: Int) {
|
||||||
|
isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPageSelected(position: Int) {
|
||||||
|
currentPosition = position
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
swipeDismissHandler = createSwipeToDismissHandler()
|
||||||
|
rootContainer.setOnTouchListener(swipeDismissHandler)
|
||||||
|
rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 }
|
||||||
|
|
||||||
|
scaleDetector = createScaleGestureDetector()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 && overlayView?.dispatchTouchEvent(event) == 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 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())
|
||||||
|
|
||||||
|
|
||||||
|
protected open fun shouldAnimateDismiss(): Boolean = true
|
||||||
|
|
||||||
|
protected open fun animateClose() {
|
||||||
|
window.statusBarColor = Color.TRANSPARENT
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.attachment_viewer
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
|
||||||
|
abstract class BaseViewHolder constructor(itemView: View) :
|
||||||
|
RecyclerView.ViewHolder(itemView) {
|
||||||
|
|
||||||
|
abstract fun bind(attachmentInfo: AttachmentInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentViewHolder constructor(itemView: View) :
|
||||||
|
BaseViewHolder(itemView) {
|
||||||
|
|
||||||
|
override fun bind(attachmentInfo: AttachmentInfo) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//class AttachmentsAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : FragmentStateAdapter(fragmentManager, lifecycle) {
|
||||||
|
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 -> ImageViewHolder(itemView)
|
||||||
|
else -> AttachmentViewHolder(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.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)
|
||||||
|
if (it is AttachmentInfo.Image) {
|
||||||
|
attachmentSourceProvider?.loadImage(holder as ImageViewHolder, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isScaled(position: Int): Boolean {
|
||||||
|
val holder = recyclerView?.findViewHolderForAdapterPosition(position)
|
||||||
|
if (holder is ImageViewHolder) {
|
||||||
|
return holder.touchImageView.attacher.scale > 1f
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// override fun getItemCount(): Int {
|
||||||
|
// return 8
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun createFragment(position: Int): Fragment {
|
||||||
|
// // Return a NEW fragment instance in createFragment(int)
|
||||||
|
// val fragment = DemoObjectFragment()
|
||||||
|
// fragment.arguments = Bundle().apply {
|
||||||
|
// // Our object is just an integer :-P
|
||||||
|
// putInt(ARG_OBJECT, position + 1)
|
||||||
|
// }
|
||||||
|
// return fragment
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//private const val ARG_OBJECT = "object"
|
||||||
|
//
|
||||||
|
//// Instances of this class are fragments representing a single
|
||||||
|
//// object in our collection.
|
||||||
|
//class DemoObjectFragment : Fragment() {
|
||||||
|
//
|
||||||
|
// override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
|
// return inflater.inflate(R.layout.view_image_attachment, container, false)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
// arguments?.takeIf { it.containsKey(ARG_OBJECT) }?.apply {
|
||||||
|
// val textView: TextView = view.findViewById(R.id.testPage)
|
||||||
|
// textView.text = getInt(ARG_OBJECT).toString()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.attachment_viewer
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import com.bumptech.glide.request.target.CustomViewTarget
|
||||||
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
import com.github.chrisbanes.photoview.PhotoView
|
||||||
|
|
||||||
|
class ImageViewHolder 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
val customTargetView = object : CustomViewTarget<PhotoView, Drawable>(touchImageView) {
|
||||||
|
|
||||||
|
override fun onResourceLoading(placeholder: Drawable?) {
|
||||||
|
imageLoaderProgress.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||||
|
imageLoaderProgress.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceCleared(placeholder: Drawable?) {
|
||||||
|
touchImageView.setImageDrawable(placeholder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||||
|
imageLoaderProgress.isVisible = false
|
||||||
|
// Glide mess up the view size :/
|
||||||
|
touchImageView.updateLayoutParams {
|
||||||
|
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||||
|
height = LinearLayout.LayoutParams.MATCH_PARENT
|
||||||
|
}
|
||||||
|
touchImageView.setImageDrawable(resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bind(attachmentInfo: AttachmentInfo) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* 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.attachment_viewer
|
||||||
|
|
||||||
|
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,90 @@
|
||||||
|
/*
|
||||||
|
* 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.attachment_viewer
|
||||||
|
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* 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.attachment_viewer
|
||||||
|
|
||||||
|
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,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">
|
||||||
|
|
||||||
|
<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,26 @@
|
||||||
|
<?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">
|
||||||
|
|
||||||
|
<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_gravity="center" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/videoControlIcon"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</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>
|
3
attachment-viewer/src/main/res/values/dimens.xml
Normal file
3
attachment-viewer/src/main/res/values/dimens.xml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<resources>
|
||||||
|
<dimen name="fab_margin">16dp</dimen>
|
||||||
|
</resources>
|
11
attachment-viewer/src/main/res/values/strings.xml
Normal file
11
attachment-viewer/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<resources>
|
||||||
|
<string name="title_activity_attachement_viewer">AttachementViewerActivity</string>
|
||||||
|
<!-- Strings used for fragments for navigation -->
|
||||||
|
<string name="first_fragment_label">First Fragment</string>
|
||||||
|
<string name="second_fragment_label">Second Fragment</string>
|
||||||
|
<string name="next">Next</string>
|
||||||
|
<string name="previous">Previous</string>
|
||||||
|
|
||||||
|
<string name="hello_first_fragment">Hello first fragment</string>
|
||||||
|
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
|
||||||
|
</resources>
|
12
attachment-viewer/src/main/res/values/styles.xml
Normal file
12
attachment-viewer/src/main/res/values/styles.xml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Theme.Transparent" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||||
|
<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>
|
|
@ -39,6 +39,8 @@ allprojects {
|
||||||
includeGroupByRegex "com\\.github\\.yalantis"
|
includeGroupByRegex "com\\.github\\.yalantis"
|
||||||
// JsonViewer
|
// JsonViewer
|
||||||
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
||||||
|
// PhotoView
|
||||||
|
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maven {
|
maven {
|
||||||
|
|
|
@ -39,4 +39,6 @@ interface TimelineService {
|
||||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||||
|
|
||||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||||
|
|
||||||
|
fun getAttachementMessages() : List<TimelineEvent>
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,19 +21,24 @@ import androidx.lifecycle.Transformations
|
||||||
import com.squareup.inject.assisted.Assisted
|
import com.squareup.inject.assisted.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
|
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
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.TimelineEvent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineService
|
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.session.room.timeline.TimelineSettings
|
||||||
import im.vector.matrix.android.api.util.Optional
|
import im.vector.matrix.android.api.util.Optional
|
||||||
import im.vector.matrix.android.api.util.toOptional
|
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.ReadReceiptsSummaryMapper
|
||||||
import im.vector.matrix.android.internal.database.mapper.TimelineEventMapper
|
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.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.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.util.fetchCopyMap
|
import im.vector.matrix.android.internal.util.fetchCopyMap
|
||||||
|
import io.realm.Sort
|
||||||
|
import io.realm.kotlin.where
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
|
||||||
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
|
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
|
||||||
|
@ -73,10 +78,10 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||||
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
||||||
return monarchy
|
return monarchy
|
||||||
.fetchCopyMap({
|
.fetchCopyMap({
|
||||||
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
|
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
|
||||||
}, { entity, _ ->
|
}, { entity, _ ->
|
||||||
timelineEventMapper.map(entity)
|
timelineEventMapper.map(entity)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
|
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
|
||||||
|
@ -88,4 +93,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||||
events.firstOrNull().toOptional()
|
events.firstOrNull().toOptional()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getAttachementMessages(): 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() } }
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'
|
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch', ':attachment-viewer'
|
||||||
include ':multipicker'
|
include ':multipicker'
|
|
@ -279,6 +279,7 @@ dependencies {
|
||||||
implementation project(":matrix-sdk-android-rx")
|
implementation project(":matrix-sdk-android-rx")
|
||||||
implementation project(":diff-match-patch")
|
implementation project(":diff-match-patch")
|
||||||
implementation project(":multipicker")
|
implementation project(":multipicker")
|
||||||
|
implementation project(":attachment-viewer")
|
||||||
implementation 'com.android.support:multidex:1.0.3'
|
implementation 'com.android.support:multidex:1.0.3'
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
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:GlideImageLoader:$big_image_viewer_version"
|
||||||
implementation "com.github.piasy:ProgressPieIndicator:$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.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"
|
implementation "com.github.bumptech.glide:glide:$glide_version"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
kapt "com.github.bumptech.glide:compiler:$glide_version"
|
||||||
implementation 'com.danikula:videocache:2.7.1'
|
implementation 'com.danikula:videocache:2.7.1'
|
||||||
|
|
|
@ -85,6 +85,11 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".features.media.ImageMediaViewerActivity" />
|
<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.media.BigImageViewerActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.rageshake.BugReportActivity"
|
android:name=".features.rageshake.BugReportActivity"
|
||||||
|
|
|
@ -48,6 +48,7 @@ import im.vector.riotx.features.invite.InviteUsersToRoomActivity
|
||||||
import im.vector.riotx.features.invite.VectorInviteView
|
import im.vector.riotx.features.invite.VectorInviteView
|
||||||
import im.vector.riotx.features.link.LinkHandlerActivity
|
import im.vector.riotx.features.link.LinkHandlerActivity
|
||||||
import im.vector.riotx.features.login.LoginActivity
|
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.BigImageViewerActivity
|
||||||
import im.vector.riotx.features.media.ImageMediaViewerActivity
|
import im.vector.riotx.features.media.ImageMediaViewerActivity
|
||||||
import im.vector.riotx.features.media.VideoMediaViewerActivity
|
import im.vector.riotx.features.media.VideoMediaViewerActivity
|
||||||
|
@ -135,6 +136,7 @@ interface ScreenComponent {
|
||||||
fun inject(activity: ReviewTermsActivity)
|
fun inject(activity: ReviewTermsActivity)
|
||||||
fun inject(activity: WidgetActivity)
|
fun inject(activity: WidgetActivity)
|
||||||
fun inject(activity: VectorCallActivity)
|
fun inject(activity: VectorCallActivity)
|
||||||
|
fun inject(activity: VectorAttachmentViewerActivity)
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* BottomSheets
|
* BottomSheets
|
||||||
|
|
|
@ -1171,7 +1171,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
|
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
|
||||||
navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
|
navigator.openImageViewer(requireActivity(), roomDetailArgs.roomId, mediaData, view) { pairs ->
|
||||||
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
|
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
|
||||||
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
|
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,13 @@ package im.vector.riotx.features.media
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import com.bumptech.glide.load.DataSource
|
import com.bumptech.glide.load.DataSource
|
||||||
import com.bumptech.glide.load.engine.GlideException
|
import com.bumptech.glide.load.engine.GlideException
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.bumptech.glide.request.RequestListener
|
import com.bumptech.glide.request.RequestListener
|
||||||
|
import com.bumptech.glide.request.target.CustomViewTarget
|
||||||
import com.bumptech.glide.request.target.Target
|
import com.bumptech.glide.request.target.Target
|
||||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
|
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView.ORIENTATION_USE_EXIF
|
||||||
import com.github.piasy.biv.view.BigImageView
|
import com.github.piasy.biv.view.BigImageView
|
||||||
|
@ -93,6 +95,25 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
.into(imageView)
|
.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) {
|
fun renderFitTarget(data: Data, mode: Mode, imageView: ImageView, callback: ((Boolean) -> Unit)? = null) {
|
||||||
val size = processSize(data, mode)
|
val size = processSize(data, mode)
|
||||||
|
|
||||||
|
@ -122,6 +143,48 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
|
||||||
.into(imageView)
|
.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> {
|
private fun createGlideRequest(data: Data, mode: Mode, imageView: ImageView, size: Size): GlideRequest<Drawable> {
|
||||||
return if (data.elementToDecrypt != null) {
|
return if (data.elementToDecrypt != null) {
|
||||||
// Encrypted image
|
// Encrypted image
|
||||||
|
|
|
@ -91,6 +91,8 @@ class ImageMediaViewerActivity : VectorBaseActivity() {
|
||||||
encryptedImageView.isVisible = false
|
encryptedImageView.isVisible = false
|
||||||
// Postpone transaction a bit until thumbnail is loaded
|
// Postpone transaction a bit until thumbnail is loaded
|
||||||
supportPostponeEnterTransition()
|
supportPostponeEnterTransition()
|
||||||
|
|
||||||
|
// We are not passing the exact same image that in the
|
||||||
imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) {
|
imageContentRenderer.renderFitTarget(mediaData, ImageContentRenderer.Mode.THUMBNAIL, imageTransitionView) {
|
||||||
// Proceed with transaction
|
// Proceed with transaction
|
||||||
scheduleStartPostponedTransition(imageTransitionView)
|
scheduleStartPostponedTransition(imageTransitionView)
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* 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.graphics.drawable.Drawable
|
||||||
|
import com.bumptech.glide.request.target.CustomViewTarget
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
|
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.attachment_viewer.AttachmentInfo
|
||||||
|
import im.vector.riotx.attachment_viewer.AttachmentSourceProvider
|
||||||
|
import im.vector.riotx.attachment_viewer.ImageViewHolder
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class RoomAttachmentProvider(
|
||||||
|
private val attachments: List<TimelineEvent>,
|
||||||
|
private val initialIndex: Int,
|
||||||
|
private val imageContentRenderer: ImageContentRenderer
|
||||||
|
) : AttachmentSourceProvider {
|
||||||
|
|
||||||
|
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
|
||||||
|
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
|
||||||
|
)
|
||||||
|
AttachmentInfo.Image(
|
||||||
|
content?.url ?: "",
|
||||||
|
data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) {
|
||||||
|
(info.data as? ImageContentRenderer.Data)?.let {
|
||||||
|
imageContentRenderer.render(it, holder.touchImageView, holder.customTargetView as CustomViewTarget<*, Drawable>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// override fun loadImage(holder: ImageViewHolder, info: AttachmentInfo.Image) {
|
||||||
|
// (info.data as? ImageContentRenderer.Data)?.let {
|
||||||
|
// imageContentRenderer.render(it, ImageContentRenderer.Mode.FULL_SIZE, holder.touchImageView)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoomAttachmentProviderFactory @Inject constructor(
|
||||||
|
private val imageContentRenderer: ImageContentRenderer
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun createProvider(attachments: List<TimelineEvent>, initialIndex: Int): RoomAttachmentProvider {
|
||||||
|
return RoomAttachmentProvider(attachments, initialIndex, imageContentRenderer)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
/*
|
||||||
|
* 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.transition.addListener
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.transition.Transition
|
||||||
|
import im.vector.riotx.attachment_viewer.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.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() {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Args(
|
||||||
|
val roomId: String?,
|
||||||
|
val eventId: String,
|
||||||
|
val sharedTransitionName: String?
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var sessionHolder: ActiveSessionHolder
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var dataSourceFactory: RoomAttachmentProviderFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var imageContentRenderer: ImageContentRenderer
|
||||||
|
|
||||||
|
private lateinit var screenComponent: ScreenComponent
|
||||||
|
|
||||||
|
private var initialIndex = 0
|
||||||
|
private var isAnimatingOut = false
|
||||||
|
|
||||||
|
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")
|
||||||
|
val session = sessionHolder.getSafeActiveSession() ?: return Unit.also { finish() }
|
||||||
|
|
||||||
|
val room = args.roomId?.let { session.getRoom(it) }
|
||||||
|
val events = room?.getAttachementMessages() ?: emptyList()
|
||||||
|
val index = events.indexOfFirst { it.eventId == args.eventId }
|
||||||
|
initialIndex = index
|
||||||
|
|
||||||
|
|
||||||
|
if (savedInstanceState == null && addTransitionListener()) {
|
||||||
|
args.sharedTransitionName?.let {
|
||||||
|
ViewCompat.setTransitionName(imageTransitionView, it)
|
||||||
|
transitionImageContainer.isVisible = true
|
||||||
|
|
||||||
|
// Postpone transaction a bit until thumbnail is loaded
|
||||||
|
val mediaData: ImageContentRenderer.Data? = intent.getParcelableExtra(EXTRA_IMAGE_DATA)
|
||||||
|
if (mediaData != null) {
|
||||||
|
// will be shown at end of transition
|
||||||
|
pager2.isInvisible = true
|
||||||
|
supportPostponeEnterTransition()
|
||||||
|
imageContentRenderer.renderThumbnailDontTransform(mediaData, imageTransitionView) {
|
||||||
|
// Proceed with transaction
|
||||||
|
scheduleStartPostponedTransition(imageTransitionView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSourceProvider(dataSourceFactory.createProvider(events, index))
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
pager2.setCurrentItem(index, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
if (!isAnimatingOut) {
|
||||||
|
// The listener is also called when we are exiting
|
||||||
|
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"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, mediaData: ImageContentRenderer.Data, roomId: String?, eventId: String, sharedTransitionName: String?) = Intent(context, VectorAttachmentViewerActivity::class.java).also {
|
||||||
|
it.putExtra(EXTRA_ARGS, Args(roomId, eventId, sharedTransitionName))
|
||||||
|
it.putExtra(EXTRA_IMAGE_DATA, mediaData)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,11 +49,7 @@ 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.detail.widget.WidgetRequestCodes
|
||||||
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
|
import im.vector.riotx.features.home.room.filtered.FilteredRoomsActivity
|
||||||
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
|
import im.vector.riotx.features.invite.InviteUsersToRoomActivity
|
||||||
import im.vector.riotx.features.media.BigImageViewerActivity
|
import im.vector.riotx.features.media.*
|
||||||
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.roomdirectory.RoomDirectoryActivity
|
import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
||||||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
||||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
|
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
|
||||||
|
@ -89,7 +85,8 @@ class DefaultNavigator @Inject constructor(
|
||||||
|
|
||||||
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
|
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
|
||||||
val session = sessionHolder.getSafeActiveSession() ?: return
|
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()
|
(tx as? IncomingSasVerificationTransaction)?.performAccept()
|
||||||
if (context is VectorBaseActivity) {
|
if (context is VectorBaseActivity) {
|
||||||
VerificationBottomSheet.withArgs(
|
VerificationBottomSheet.withArgs(
|
||||||
|
@ -216,7 +213,8 @@ class DefaultNavigator @Inject constructor(
|
||||||
?.let { avatarUrl ->
|
?.let { avatarUrl ->
|
||||||
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
|
val intent = BigImageViewerActivity.newIntent(activity, matrixItem.getBestName(), avatarUrl)
|
||||||
val options = sharedElement?.let {
|
val options = sharedElement?.let {
|
||||||
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it) ?: "")
|
ActivityOptionsCompat.makeSceneTransitionAnimation(activity, it, ViewCompat.getTransitionName(it)
|
||||||
|
?: "")
|
||||||
}
|
}
|
||||||
activity.startActivity(intent, options?.toBundle())
|
activity.startActivity(intent, options?.toBundle())
|
||||||
}
|
}
|
||||||
|
@ -244,22 +242,38 @@ class DefaultNavigator @Inject constructor(
|
||||||
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
|
context.startActivity(WidgetActivity.newIntent(context, widgetArgs))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?) {
|
override fun openImageViewer(activity: Activity, roomId: String?, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?) {
|
||||||
val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
|
VectorAttachmentViewerActivity.newIntent(activity, mediaData, roomId, mediaData.eventId, ViewCompat.getTransitionName(view)).let { intent ->
|
||||||
val pairs = ArrayList<Pair<View, String>>()
|
val pairs = ArrayList<Pair<View, String>>()
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
|
activity.window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
|
||||||
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
activity.window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let {
|
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
|
||||||
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
|
options?.invoke(pairs)
|
||||||
}
|
|
||||||
}
|
|
||||||
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
|
|
||||||
options?.invoke(pairs)
|
|
||||||
|
|
||||||
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
|
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, *pairs.toTypedArray()).toBundle()
|
||||||
activity.startActivity(intent, bundle)
|
activity.startActivity(intent, bundle)
|
||||||
|
}
|
||||||
|
// val intent = ImageMediaViewerActivity.newIntent(activity, mediaData, ViewCompat.getTransitionName(view))
|
||||||
|
// 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) {
|
override fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data) {
|
||||||
|
|
|
@ -91,7 +91,7 @@ interface Navigator {
|
||||||
|
|
||||||
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
|
fun openRoomWidget(context: Context, roomId: String, widget: Widget)
|
||||||
|
|
||||||
fun openImageViewer(activity: Activity, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?)
|
fun openImageViewer(activity: Activity, roomId: String?, mediaData: ImageContentRenderer.Data, view: View, options: ((MutableList<Pair<View, String>>) -> Unit)?)
|
||||||
|
|
||||||
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
|
fun openVideoViewer(activity: Activity, mediaData: VideoContentRenderer.Data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import com.tapadoo.alerter.Alerter
|
||||||
import com.tapadoo.alerter.OnHideAlertListener
|
import com.tapadoo.alerter.OnHideAlertListener
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.themes.ThemeUtils
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -83,7 +84,7 @@ class PopupAlertManager @Inject constructor(private val avatarRenderer: Lazy<Ava
|
||||||
setLightStatusBar()
|
setLightStatusBar()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (currentAlerter?.shouldBeDisplayedIn?.invoke(activity) == false) {
|
if (currentAlerter?.shouldBeDisplayedIn?.invoke(activity) == false || activity !is VectorBaseActivity) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,7 @@ class RoomUploadsMediaFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) {
|
override fun onOpenImageClicked(view: View, mediaData: ImageContentRenderer.Data) {
|
||||||
navigator.openImageViewer(requireActivity(), mediaData, view, null)
|
navigator.openImageViewer(requireActivity(), null, mediaData, view, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
|
override fun onOpenVideoClicked(view: View, mediaData: VideoContentRenderer.Data) {
|
||||||
|
|
|
@ -38,4 +38,10 @@ sealed class ActivityOtherThemes(@StyleRes val dark: Int,
|
||||||
R.style.AppTheme_AttachmentsPreview,
|
R.style.AppTheme_AttachmentsPreview,
|
||||||
R.style.AppTheme_AttachmentsPreview
|
R.style.AppTheme_AttachmentsPreview
|
||||||
)
|
)
|
||||||
|
|
||||||
|
object VectorAttachmentsPreview : ActivityOtherThemes(
|
||||||
|
R.style.AppTheme_Transparent,
|
||||||
|
R.style.AppTheme_Transparent,
|
||||||
|
R.style.AppTheme_Transparent
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,4 +10,15 @@
|
||||||
|
|
||||||
<style name="AppTheme.AttachmentsPreview" parent="AppTheme.Base.Black"/>
|
<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>
|
</resources>
|
Loading…
Add table
Reference in a new issue