Refactoring/ create custom view for composerLayout in timeline

+ simplify quote/edit composer preview animation
This commit is contained in:
Valere 2019-05-25 14:49:35 +02:00
parent 3c16701766
commit b45cc0e63f
8 changed files with 224 additions and 123 deletions

View File

@ -1,41 +0,0 @@
package im.vector.riotredesign.core.utils

import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.transition.ChangeBounds
import androidx.transition.Transition
import androidx.transition.TransitionManager


inline fun ConstraintLayout.updateConstraintSet(@LayoutRes layoutId: Int,
rootLayoutForAnimation: ConstraintLayout? = null,
noinline onAnimationEnd: (() -> Unit)? = null) {
if (rootLayoutForAnimation != null) {
val transition = ChangeBounds()
transition.interpolator = OvershootInterpolator()
transition.addListener(object : Transition.TransitionListener {
override fun onTransitionResume(transition: Transition) {
}

override fun onTransitionPause(transition: Transition) {
}

override fun onTransitionCancel(transition: Transition) {
}

override fun onTransitionStart(transition: Transition) {
}

override fun onTransitionEnd(transition: Transition) {
onAnimationEnd?.invoke()
}
})
TransitionManager.beginDelayedTransition(rootLayoutForAnimation, transition)
}
ConstraintSet().also {
it.clone(this@updateConstraintSet.context, layoutId)
it.applyTo(this@updateConstraintSet)
}
}

View File

@ -32,11 +32,9 @@ import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
@ -79,6 +77,7 @@ import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.HomeModule
import im.vector.riotredesign.features.home.HomePermalinkHandler
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerActions
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewModel
import im.vector.riotredesign.features.home.room.detail.composer.TextComposerViewState
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
@ -96,7 +95,7 @@ import im.vector.riotredesign.features.media.VideoMediaViewerActivity
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.include_composer_layout.*
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
import org.koin.android.ext.android.inject
import org.koin.android.scope.ext.android.bindScope
import org.koin.android.scope.ext.android.getOrCreateScope
@ -170,14 +169,8 @@ class RoomDetailFragment :

private lateinit var actionViewModel: ActionsHandler

@BindView(R.id.composer_related_message_sender)
lateinit var composerRelatedMessageTitle: TextView
@BindView(R.id.composer_related_message_preview)
lateinit var composerRelatedMessageContent: TextView
@BindView(R.id.composerLayout)
lateinit var composerLayout: ConstraintLayout
@BindView(R.id.rootConstraintLayout)
lateinit var rootConstraintLayout: ConstraintLayout
lateinit var composerLayout: TextComposerView

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
@ -211,10 +204,8 @@ class RoomDetailFragment :
commandAutocompletePolicy.enabled = true
val uid = session.sessionParams.credentials.userId
val meMember = session.getRoom(roomId)?.getRoomMember(uid)
AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composer_avatar_view)
composerLayout.updateConstraintSet(R.layout.constraint_set_composer_layout_compact, rootConstraintLayout) {
focusComposerAndShowKeyboard()
}
AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)
composerLayout.collapse()
}
SendMode.EDIT,
SendMode.QUOTE -> {
@ -225,40 +216,37 @@ class RoomDetailFragment :
return@selectSubscribe
}
//switch to expanded bar
composerRelatedMessageTitle.text = event.senderName
composerRelatedMessageTitle.setTextColor(
ContextCompat.getColor(requireContext(), AvatarRenderer.getColorFromUserId(event.root.sender
?: ""))
)
composerLayout.composerRelatedMessageTitle.apply {
text = event.senderName
setTextColor(ContextCompat.getColor(requireContext(), AvatarRenderer.getColorFromUserId(event.root.sender
?: "")))
}

//TODO this is used at several places, find way to refactor?
val messageContent: MessageContent? =
event.annotations?.editSummary?.aggregatedContent?.toModel()
?: event.root.content.toModel()
val eventTextBody = messageContent?.body
composerRelatedMessageContent.text = eventTextBody
composerLayout.composerRelatedMessageContent.text = eventTextBody


if (mode == SendMode.EDIT) {
composerEditText.setText(eventTextBody)
composer_related_message_action_image.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit))
composerLayout.composerEditText.setText(eventTextBody)
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_edit))
} else {
composerEditText.setText("")
composer_related_message_action_image.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote))
composerLayout.composerEditText.setText("")
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_quote))
}

AvatarRenderer.render(event.senderAvatar, event.root.sender
?: "", event.senderName, composer_avatar_view)
?: "", event.senderName, composerLayout.composerRelatedMessageAvatar)

composerEditText.setSelection(composerEditText.text.length)
composerLayout.updateConstraintSet(R.layout.constraint_set_composer_layout_expanded, rootConstraintLayout) {
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
composerLayout.expand {
focusComposerAndShowKeyboard()
}

view?.findViewById<ImageButton>(R.id.composer_related_message_close)?.setOnClickListener {

composerRelatedMessageTitle.text = ""
composerRelatedMessageContent.text = ""
composerEditText.setText("")
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
composerLayout.composerEditText.setText("")
roomDetailViewModel.resetSendMode()
}

@ -323,7 +311,7 @@ class RoomDetailFragment :
private fun setupComposer() {
val elevation = 6f
val backgroundDrawable = ColorDrawable(Color.WHITE)
Autocomplete.on<Command>(composerEditText)
Autocomplete.on<Command>(composerLayout.composerEditText)
.with(commandAutocompletePolicy)
.with(autocompleteCommandPresenter)
.with(elevation)
@ -343,7 +331,7 @@ class RoomDetailFragment :
.build()

autocompleteUserPresenter.callback = this
Autocomplete.on<User>(composerEditText)
Autocomplete.on<User>(composerLayout.composerEditText)
.with(CharPolicy('@', true))
.with(autocompleteUserPresenter)
.with(elevation)
@ -371,7 +359,7 @@ class RoomDetailFragment :
// Add the span
val user = session.getUser(item.userId)
val span = PillImageSpan(glideRequests, context!!, item.userId, user)
span.bind(composerEditText)
span.bind(composerLayout.composerEditText)

editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

@ -383,8 +371,8 @@ class RoomDetailFragment :
})
.build()

sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString()
composerLayout.sendButton.setOnClickListener {
val textMessage = composerLayout.composerEditText.text.toString()
if (textMessage.isNotBlank()) {
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
}
@ -392,7 +380,7 @@ class RoomDetailFragment :
}

private fun setupAttachmentButton() {
attachmentButton.setOnClickListener {
composerLayout.attachmentButton.setOnClickListener {
val intent = Intent(requireContext(), FilePickerActivity::class.java)
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
.setCheckPermission(true)
@ -479,7 +467,7 @@ class RoomDetailFragment :

val uid = session.sessionParams.credentials.userId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composer_avatar_view)
AvatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView)

} else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE
@ -511,7 +499,7 @@ class RoomDetailFragment :
is SendMessageResult.MessageSent,
is SendMessageResult.SlashCommandHandled -> {
// Clear composer
composerEditText.text = null
composerLayout.composerEditText.text = null
}
is SendMessageResult.SlashCommandError -> {
displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command))
@ -705,6 +693,7 @@ class RoomDetailFragment :
*
* @param text the text to insert.
*/
//TODO legacy, refactor
private fun insertUserDisplayNameInTextEditor(text: String?) {
//TODO move logic outside of fragment
if (null != text) {
@ -713,21 +702,21 @@ class RoomDetailFragment :
val myDisplayName = session.getUser(session.sessionParams.credentials.userId)?.displayName
if (TextUtils.equals(myDisplayName, text)) {
// current user
if (TextUtils.isEmpty(composerEditText.text)) {
composerEditText.append(Command.EMOTE.command + " ")
composerEditText.setSelection(composerEditText.text.length)
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
// vibrate = true
}
} else {
// another user
if (TextUtils.isEmpty(composerEditText.text)) {
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
// Ensure displayName will not be interpreted as a Slash command
if (text.startsWith("/")) {
composerEditText.append("\\")
composerLayout.composerEditText.append("\\")
}
composerEditText.append(sanitizeDisplayname(text)!! + ": ")
composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ")
} else {
composerEditText.text.insert(composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
}

// vibrate = true
@ -744,9 +733,9 @@ class RoomDetailFragment :
}

private fun focusComposerAndShowKeyboard() {
composerEditText.requestFocus()
composerLayout.composerEditText.requestFocus()
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
imm?.showSoftInput(composerEditText, InputMethodManager.SHOW_IMPLICIT)
imm?.showSoftInput(composerLayout.composerEditText, InputMethodManager.SHOW_IMPLICIT)
}

fun showSnackWithMessage(message: String, duration: Int = Snackbar.LENGTH_SHORT) {

View File

@ -0,0 +1,116 @@
package im.vector.riotredesign.features.home.room.detail.composer

import android.content.Context
import android.util.AttributeSet
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.AutoTransition
import androidx.transition.Transition
import androidx.transition.TransitionManager
import butterknife.BindView
import butterknife.ButterKnife
import im.vector.riotredesign.R


/**
* Encapsulate the timeline composer UX.
*
*/
class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {

@BindView(R.id.composer_related_message_sender)
lateinit var composerRelatedMessageTitle: TextView
@BindView(R.id.composer_related_message_preview)
lateinit var composerRelatedMessageContent: TextView
@BindView(R.id.composer_related_message_avatar_view)
lateinit var composerRelatedMessageAvatar: ImageView
@BindView(R.id.composer_related_message_action_image)
lateinit var composerRelatedMessageActionIcon: ImageView
@BindView(R.id.composer_related_message_close)
lateinit var composerRelatedMessageCloseButton: ImageButton
@BindView(R.id.composerEditText)
lateinit var composerEditText: EditText
@BindView(R.id.composer_avatar_view)
lateinit var composerAvatarImageView: ImageView

var currentConstraintSetId: Int = -1


init {
inflate(context, R.layout.merge_composer_layout, this)
ButterKnife.bind(this)
collapse(false)
}


fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
if (currentConstraintSetId == R.layout.constraint_set_composer_layout_compact) {
//ignore we good
return
}
currentConstraintSetId = R.layout.constraint_set_composer_layout_compact
if (animate) {
val transition = AutoTransition()
// transition.duration = 5000
transition.addListener(object : Transition.TransitionListener {

override fun onTransitionEnd(transition: Transition) {
transitionComplete?.invoke()
}

override fun onTransitionResume(transition: Transition) {}

override fun onTransitionPause(transition: Transition) {}

override fun onTransitionCancel(transition: Transition) {}

override fun onTransitionStart(transition: Transition) {}
}
)
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
}
ConstraintSet().also {
it.clone(context, currentConstraintSetId)
it.applyTo(this)
}
}

fun expand(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
if (currentConstraintSetId == R.layout.constraint_set_composer_layout_expanded) {
//ignore we good
return
}
currentConstraintSetId = R.layout.constraint_set_composer_layout_expanded
if (animate) {
val transition = AutoTransition()
// transition.duration = 5000
transition.addListener(object : Transition.TransitionListener {

override fun onTransitionEnd(transition: Transition) {
transitionComplete?.invoke()
}

override fun onTransitionResume(transition: Transition) {}

override fun onTransitionPause(transition: Transition) {}

override fun onTransitionCancel(transition: Transition) {}

override fun onTransitionStart(transition: Transition) {}
}
)
TransitionManager.beginDelayedTransition((parent as? ViewGroup ?: this), transition)
}
ConstraintSet().also {
it.clone(context, currentConstraintSetId)
it.applyTo(this)
}
}
}

View File

@ -67,6 +67,13 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes

//TODO determine if can copy, forward, reply, quote, report?
val actions = ArrayList<SimpleAction>().apply {

if (event.sendState == SendState.SENDING) {
//TODO add cancel?
return@apply
}
//TODO is downloading attachement?

this.add(SimpleAction(ACTION_ADD_REACTION, R.string.message_add_reaction, R.drawable.ic_add_reaction, event.root.eventId))
if (canCopy(type)) {
//TODO copy images? html? see ClipBoard
@ -100,8 +107,6 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel<Mes
//TODO
}

//TODO is uploading
//TODO is downloading

if (event.sendState == SendState.SENT) {


View File

@ -31,11 +31,21 @@
<View
android:id="@+id/related_message_background_bottom_separator"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_height="1dp"
android:background="?vctr_bottom_nav_background_border_color"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<ImageView
android:id="@+id/composer_related_message_avatar_view"
android:layout_width="40dp"
android:layout_height="40dp"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toStartOf="parent"
tools:src="@tools:sample/avatars" />

<TextView
android:id="@+id/composer_related_message_sender"
@ -79,6 +89,7 @@
android:background="?android:attr/selectableItemBackground"
android:src="@drawable/ic_close_round"
android:tint="@color/rosy_pink"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintStart_toEndOf="parent" />


View File

@ -22,9 +22,9 @@
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?vctr_bottom_nav_background_border_color"
app:layout_constraintTop_toTopOf="@id/related_message_backround"
app:layout_constraintEnd_toEndOf="@id/related_message_backround"
app:layout_constraintStart_toStartOf="@+id/related_message_backround" />
app:layout_constraintStart_toStartOf="@+id/related_message_backround"
app:layout_constraintTop_toTopOf="@id/related_message_backround" />

<View
android:id="@+id/related_message_background_bottom_separator"
@ -35,6 +35,20 @@
app:layout_constraintEnd_toEndOf="@id/related_message_backround"
app:layout_constraintStart_toStartOf="@+id/related_message_backround" />

<ImageView
android:id="@+id/composer_related_message_avatar_view"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@id/composer_related_message_action_image"
app:layout_constraintEnd_toStartOf="@+id/composer_related_message_sender"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/composer_related_message_sender"
tools:src="@tools:sample/avatars" />

<TextView
android:id="@+id/composer_related_message_sender"
android:layout_width="0dp"
@ -42,7 +56,7 @@
android:layout_margin="8dp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/composer_related_message_close"
app:layout_constraintStart_toEndOf="@id/composer_avatar_view"
app:layout_constraintStart_toEndOf="@id/composer_related_message_avatar_view"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/first_names" />

@ -68,9 +82,9 @@
android:alpha="1"
android:tint="?android:attr/textColorTertiary"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="@id/composer_avatar_view"
app:layout_constraintStart_toStartOf="@id/composer_avatar_view"
app:layout_constraintTop_toBottomOf="@id/composer_avatar_view"
app:layout_constraintEnd_toEndOf="@id/composer_related_message_avatar_view"
app:layout_constraintStart_toStartOf="@id/composer_related_message_avatar_view"
app:layout_constraintTop_toBottomOf="@id/composer_related_message_avatar_view"
tools:src="@drawable/ic_edit" />


@ -90,16 +104,19 @@

<ImageView
android:id="@+id/composer_avatar_view"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@id/composer_related_message_action_image"
app:layout_constraintEnd_toStartOf="@+id/composer_related_message_sender"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/composerEditText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/composer_related_message_sender"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1"
tools:src="@tools:sample/avatars" />


@ -149,7 +166,7 @@
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/sendButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/composer_avatar_view"
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
tools:text="@tools:sample/lorem" />


View File

@ -77,20 +77,20 @@
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/composerDivider"
app:layout_constraintBottom_toTopOf="@+id/composerLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
tools:listitem="@layout/item_timeline_event_text_message" />

<View
android:id="@+id/composerDivider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?vctr_list_divider_color"
app:layout_constraintBottom_toTopOf="@+id/composerLayout" />

<include layout="@layout/include_composer_layout" />
<im.vector.riotredesign.features.home.room.detail.composer.TextComposerView
android:id="@+id/composerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<im.vector.riotredesign.features.invite.VectorInviteView
android:id="@+id/inviteView"

View File

@ -1,14 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/composerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:constraintSet="@layout/constraint_set_composer_layout_compact"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
tools:constraintSet="@layout/constraint_set_composer_layout_compact"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">

<!-- ========================
/!\ Constraints for this layout are defined in external layout files that are used as constraint set for animation.
@ -35,6 +32,13 @@
android:background="?vctr_bottom_nav_background_border_color"
tools:ignore="MissingConstraints" />

<ImageView
android:id="@+id/composer_related_message_avatar_view"
android:layout_width="0dp"
android:layout_height="0dp"
tools:ignore="MissingConstraints"
tools:src="@tools:sample/avatars" />

<TextView
android:id="@+id/composer_related_message_sender"
android:layout_width="0dp"
@ -120,4 +124,4 @@
android:textSize="14sp"
tools:ignore="MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>
</merge>