Attachments: get better layout

This commit is contained in:
ganfra 2019-10-11 12:20:39 +02:00
parent 20696353b8
commit ee5ebb4b83
8 changed files with 132 additions and 59 deletions

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}
}

View File

@ -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

View File

@ -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
)

View File

@ -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")
}
}

View File

@ -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>