Attachments: get better layout
This commit is contained in:
parent
20696353b8
commit
ee5ebb4b83
@ -47,4 +47,11 @@ fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
|
||||
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
|
||||
}
|
||||
if (updateCursor) setSelection(text?.length ?: 0)
|
||||
}
|
||||
|
||||
fun View.getMeasurements(): Pair<Int, Int> {
|
||||
measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||
val width = measuredWidth
|
||||
val height = measuredHeight
|
||||
return width to height
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package im.vector.riotx.core.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
|
||||
class KeyboardStateUtils(activity: Activity) : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
private val contentView: View = activity.findViewById<View>(android.R.id.content).also {
|
||||
it.viewTreeObserver.addOnGlobalLayoutListener(this)
|
||||
}
|
||||
var isKeyboardShowing: Boolean = false
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
val rect = Rect()
|
||||
contentView.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = contentView.rootView.height
|
||||
|
||||
val keypadHeight = screenHeight - rect.bottom
|
||||
isKeyboardShowing = keypadHeight > screenHeight * 0.15
|
||||
}
|
||||
}
|
@ -67,6 +67,7 @@ const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
|
||||
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
|
||||
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
|
||||
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
|
||||
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
||||
|
||||
/**
|
||||
* Log the used permissions statuses.
|
||||
|
@ -40,16 +40,18 @@ import androidx.core.view.doOnNextLayout
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.core.extensions.getMeasurements
|
||||
import kotlin.math.max
|
||||
|
||||
private const val ANIMATION_DURATION = 250
|
||||
|
||||
class AttachmentTypeSelectorView(context: Context,
|
||||
inflater: LayoutInflater,
|
||||
var callback: Callback?)
|
||||
: PopupWindow(context) {
|
||||
|
||||
interface Callback {
|
||||
fun onTypeSelected(type: Int)
|
||||
fun onTypeSelected(type: Type)
|
||||
}
|
||||
|
||||
private val iconColorGenerator = ColorGenerator.MATERIAL
|
||||
@ -66,12 +68,12 @@ class AttachmentTypeSelectorView(context: Context,
|
||||
init {
|
||||
val root = FrameLayout(context)
|
||||
val layout = inflater.inflate(R.layout.view_attachment_type_selector, root, true)
|
||||
galleryButton = layout.findViewById<ImageButton>(R.id.attachmentGalleryButton).configure(TYPE_GALLERY)
|
||||
cameraButton = layout.findViewById<ImageButton>(R.id.attachmentCameraButton).configure(TYPE_CAMERA)
|
||||
fileButton = layout.findViewById<ImageButton>(R.id.attachmentFileButton).configure(TYPE_FILE)
|
||||
stickersButton = layout.findViewById<ImageButton>(R.id.attachmentStickersButton).configure(TYPE_STICKER)
|
||||
audioButton = layout.findViewById<ImageButton>(R.id.attachmentAudioButton).configure(TYPE_AUDIO)
|
||||
contactButton = layout.findViewById<ImageButton>(R.id.attachmentContactButton).configure(TYPE_CONTACT)
|
||||
galleryButton = layout.findViewById<ImageButton>(R.id.attachmentGalleryButton).configure(Type.GALLERY)
|
||||
cameraButton = layout.findViewById<ImageButton>(R.id.attachmentCameraButton).configure(Type.CAMERA)
|
||||
fileButton = layout.findViewById<ImageButton>(R.id.attachmentFileButton).configure(Type.FILE)
|
||||
stickersButton = layout.findViewById<ImageButton>(R.id.attachmentStickersButton).configure(Type.STICKER)
|
||||
audioButton = layout.findViewById<ImageButton>(R.id.attachmentAudioButton).configure(Type.AUDIO)
|
||||
contactButton = layout.findViewById<ImageButton>(R.id.attachmentContactButton).configure(Type.CONTACT)
|
||||
contentView = layout
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
@ -82,8 +84,20 @@ class AttachmentTypeSelectorView(context: Context,
|
||||
isTouchable = true
|
||||
}
|
||||
|
||||
fun show(anchor: View) {
|
||||
showAtLocation(anchor, Gravity.BOTTOM, 0, 0)
|
||||
fun show(anchor: View, isKeyboardOpen: Boolean) {
|
||||
this.anchor = anchor
|
||||
val anchorCoordinates = IntArray(2)
|
||||
anchor.getLocationOnScreen(anchorCoordinates)
|
||||
if (isKeyboardOpen) {
|
||||
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height)
|
||||
} else {
|
||||
val contentViewHeight = if (contentView.height == 0) {
|
||||
contentView.getMeasurements().second
|
||||
} else {
|
||||
contentView.height
|
||||
}
|
||||
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight)
|
||||
}
|
||||
contentView.doOnNextLayout {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateWindowInCircular(anchor, contentView)
|
||||
@ -124,10 +138,10 @@ class AttachmentTypeSelectorView(context: Context,
|
||||
private fun animateWindowInCircular(anchor: View?, contentView: View) {
|
||||
val coordinates = getClickCoordinates(anchor, contentView)
|
||||
val animator = ViewAnimationUtils.createCircularReveal(contentView,
|
||||
coordinates.first,
|
||||
coordinates.second,
|
||||
0f,
|
||||
max(contentView.width, contentView.height).toFloat())
|
||||
coordinates.first,
|
||||
coordinates.second,
|
||||
0f,
|
||||
max(contentView.width, contentView.height).toFloat())
|
||||
animator.duration = ANIMATION_DURATION.toLong()
|
||||
animator.start()
|
||||
}
|
||||
@ -142,10 +156,10 @@ class AttachmentTypeSelectorView(context: Context,
|
||||
private fun animateWindowOutCircular(anchor: View?, contentView: View) {
|
||||
val coordinates = getClickCoordinates(anchor, contentView)
|
||||
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
||||
coordinates.first,
|
||||
coordinates.second,
|
||||
max(getContentView().width, getContentView().height).toFloat(),
|
||||
0f)
|
||||
coordinates.first,
|
||||
coordinates.second,
|
||||
max(getContentView().width, getContentView().height).toFloat(),
|
||||
0f)
|
||||
|
||||
animator.duration = ANIMATION_DURATION.toLong()
|
||||
animator.addListener(object : AnimatorListenerAdapter() {
|
||||
@ -182,13 +196,13 @@ class AttachmentTypeSelectorView(context: Context,
|
||||
return Pair(x, y)
|
||||
}
|
||||
|
||||
private fun ImageButton.configure(type: Int): ImageButton {
|
||||
private fun ImageButton.configure(type: Type): ImageButton {
|
||||
this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type))
|
||||
this.setOnClickListener(TypeClickListener(type))
|
||||
return this
|
||||
}
|
||||
|
||||
private inner class TypeClickListener(private val type: Int) : View.OnClickListener {
|
||||
private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
dismiss()
|
||||
@ -197,16 +211,21 @@ class AttachmentTypeSelectorView(context: Context,
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
enum class Type {
|
||||
|
||||
CAMERA,
|
||||
GALLERY,
|
||||
FILE,
|
||||
STICKER,
|
||||
AUDIO,
|
||||
CONTACT;
|
||||
|
||||
fun requirePermission(): Boolean {
|
||||
return this != CAMERA && this != STICKER
|
||||
}
|
||||
|
||||
const val TYPE_CAMERA = 0
|
||||
const val TYPE_GALLERY = 1
|
||||
const val TYPE_FILE = 2
|
||||
const val TYPE_STICKER = 3
|
||||
const val TYPE_AUDIO = 4
|
||||
const val TYPE_CONTACT = 5
|
||||
|
||||
private const val ANIMATION_DURATION = 250
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.riotx.core.platform.Restorable
|
||||
|
||||
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
||||
private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
|
||||
|
||||
/**
|
||||
* This class helps to handle attachments by providing simple methods.
|
||||
@ -49,6 +50,7 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi
|
||||
}
|
||||
|
||||
private var capturePath: String? = null
|
||||
var pendingType: AttachmentTypeSelectorView.Type? = null
|
||||
|
||||
private val imagePicker by lazy {
|
||||
pickerManagerFactory.createImagePicker()
|
||||
@ -76,6 +78,9 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi
|
||||
capturePath?.also {
|
||||
outState.putString(CAPTURE_PATH_KEY, it)
|
||||
}
|
||||
pendingType?.also {
|
||||
outState.putSerializable(PENDING_TYPE_KEY, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
||||
@ -83,6 +88,7 @@ class AttachmentsHelper private constructor(private val pickerManagerFactory: Pi
|
||||
if (capturePath != null) {
|
||||
cameraImagePicker.reinitialize(capturePath)
|
||||
}
|
||||
pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type
|
||||
}
|
||||
|
||||
// Public Methods
|
||||
|
@ -28,7 +28,7 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
|
||||
mimeType = mimeType,
|
||||
type = mapType(),
|
||||
size = size,
|
||||
date = createdAt.time,
|
||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||
name = displayName
|
||||
)
|
||||
}
|
||||
@ -39,7 +39,7 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
|
||||
mimeType = mimeType,
|
||||
type = mapType(),
|
||||
size = size,
|
||||
date = createdAt.time,
|
||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||
name = displayName,
|
||||
duration = duration
|
||||
)
|
||||
@ -61,9 +61,9 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
||||
type = mapType(),
|
||||
name = displayName,
|
||||
size = size,
|
||||
height = height.toLong(),
|
||||
width = width.toLong(),
|
||||
date = createdAt.time
|
||||
height = height?.toLong(),
|
||||
width = width?.toLong(),
|
||||
date = createdAt?.time ?: System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
@ -73,9 +73,9 @@ fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
|
||||
mimeType = mimeType,
|
||||
type = ContentAttachmentData.Type.VIDEO,
|
||||
size = size,
|
||||
date = createdAt.time,
|
||||
height = height.toLong(),
|
||||
width = width.toLong(),
|
||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||
height = height?.toLong(),
|
||||
width = width?.toLong(),
|
||||
duration = duration,
|
||||
name = displayName
|
||||
)
|
||||
|
@ -115,6 +115,7 @@ import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
|
||||
import org.commonmark.parser.Parser
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.security.Key
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@ -198,6 +199,7 @@ class RoomDetailFragment :
|
||||
private lateinit var actionViewModel: ActionsHandler
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||
private lateinit var keyboardStateUtils: KeyboardStateUtils
|
||||
|
||||
@BindView(R.id.composerLayout)
|
||||
lateinit var composerLayout: TextComposerView
|
||||
@ -205,6 +207,7 @@ class RoomDetailFragment :
|
||||
|
||||
private var lockSendButton = false
|
||||
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
@ -213,6 +216,7 @@ class RoomDetailFragment :
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||
setupToolbar(roomToolbar)
|
||||
setupRecyclerView()
|
||||
setupComposer()
|
||||
@ -310,9 +314,9 @@ class RoomDetailFragment :
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(getString(R.string.error_file_too_big,
|
||||
error.filename,
|
||||
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
|
||||
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
|
||||
error.filename,
|
||||
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
|
||||
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
|
||||
))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
@ -389,11 +393,11 @@ class RoomDetailFragment :
|
||||
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
|
||||
val parser = Parser.builder().build()
|
||||
val document = parser.parse(messageContent.formattedBody
|
||||
?: messageContent.body)
|
||||
?: messageContent.body)
|
||||
formattedBody = eventHtmlRenderer.render(document)
|
||||
}
|
||||
composerLayout.composerRelatedMessageContent.text = formattedBody
|
||||
?: nonFormattedBody
|
||||
?: nonFormattedBody
|
||||
|
||||
updateComposerText(defaultContent)
|
||||
|
||||
@ -402,11 +406,11 @@ class RoomDetailFragment :
|
||||
|
||||
|
||||
avatarRenderer.render(event.senderAvatar, event.root.senderId
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)
|
||||
avatarRenderer.render(event.senderAvatar,
|
||||
event.root.senderId ?: "",
|
||||
event.senderName,
|
||||
composerLayout.composerRelatedMessageAvatar)
|
||||
event.root.senderId ?: "",
|
||||
event.senderName,
|
||||
composerLayout.composerRelatedMessageAvatar)
|
||||
composerLayout.expand {
|
||||
//need to do it here also when not using quick reply
|
||||
focusComposerAndShowKeyboard()
|
||||
@ -443,9 +447,9 @@ class RoomDetailFragment :
|
||||
when (requestCode) {
|
||||
REACTION_SELECT_REQUEST_CODE -> {
|
||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||
?: return
|
||||
?: return
|
||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||
?: return
|
||||
?: return
|
||||
//TODO check if already reacted with that?
|
||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId))
|
||||
}
|
||||
@ -453,6 +457,7 @@ class RoomDetailFragment :
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************
|
||||
|
||||
|
||||
@ -624,7 +629,7 @@ class RoomDetailFragment :
|
||||
if (!::attachmentTypeSelector.isInitialized) {
|
||||
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this)
|
||||
}
|
||||
attachmentTypeSelector.show(composerLayout.attachmentButton)
|
||||
attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
|
||||
}
|
||||
}
|
||||
|
||||
@ -825,11 +830,16 @@ class RoomDetailFragment :
|
||||
if (allGranted(grantResults)) {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) {
|
||||
val action = roomDetailViewModel.pendingAction
|
||||
|
||||
if (action != null) {
|
||||
roomDetailViewModel.pendingAction = null
|
||||
roomDetailViewModel.process(action)
|
||||
}
|
||||
} else if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT) {
|
||||
val pendingType = attachmentsHelper.pendingType
|
||||
if (pendingType != null) {
|
||||
attachmentsHelper.pendingType = null
|
||||
launchAttachmentProcess(pendingType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1109,15 +1119,22 @@ class RoomDetailFragment :
|
||||
|
||||
// AttachmentTypeSelectorView.Callback
|
||||
|
||||
override fun onTypeSelected(type: Int) {
|
||||
when (type) {
|
||||
AttachmentTypeSelectorView.TYPE_CAMERA -> attachmentsHelper.openCamera()
|
||||
AttachmentTypeSelectorView.TYPE_FILE -> attachmentsHelper.selectFile()
|
||||
AttachmentTypeSelectorView.TYPE_GALLERY -> attachmentsHelper.selectGallery()
|
||||
AttachmentTypeSelectorView.TYPE_AUDIO -> attachmentsHelper.selectAudio()
|
||||
AttachmentTypeSelectorView.TYPE_CONTACT -> vectorBaseActivity.notImplemented("Picking contacts")
|
||||
AttachmentTypeSelectorView.TYPE_STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
||||
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
|
||||
if (!type.requirePermission() || checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) {
|
||||
launchAttachmentProcess(type)
|
||||
} else {
|
||||
attachmentsHelper.pendingType = type
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
||||
when (type) {
|
||||
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
|
||||
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
|
||||
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
|
||||
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
|
||||
AttachmentTypeSelectorView.Type.CONTACT -> vectorBaseActivity.notImplemented("Picking contacts")
|
||||
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,8 +326,8 @@
|
||||
|
||||
|
||||
<style name="AttachmentTypeSelectorButton">
|
||||
<item name="android:layout_width">48dp</item>
|
||||
<item name="android:layout_height">48dp</item>
|
||||
<item name="android:layout_width">56dp</item>
|
||||
<item name="android:layout_height">56dp</item>
|
||||
<item name="android:scaleType">center</item>
|
||||
</style>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user