From b45cc0e63fa5fa262580286f023e565b1351f8b7 Mon Sep 17 00:00:00 2001 From: Valere Date: Sat, 25 May 2019 14:49:35 +0200 Subject: [PATCH] Refactoring/ create custom view for composerLayout in timeline + simplify quote/edit composer preview animation --- .../riotredesign/core/utils/AnimationUtils.kt | 41 ------- .../home/room/detail/RoomDetailFragment.kt | 89 ++++++-------- .../room/detail/composer/TextComposerView.kt | 116 ++++++++++++++++++ .../timeline/action/MessageMenuViewModel.kt | 9 +- ...constraint_set_composer_layout_compact.xml | 17 ++- ...onstraint_set_composer_layout_expanded.xml | 41 +++++-- .../main/res/layout/fragment_room_detail.xml | 16 +-- ...r_layout.xml => merge_composer_layout.xml} | 18 +-- 8 files changed, 224 insertions(+), 123 deletions(-) delete mode 100644 vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt create mode 100644 vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerView.kt rename vector/src/main/res/layout/{include_composer_layout.xml => merge_composer_layout.xml} (90%) diff --git a/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt b/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt deleted file mode 100644 index e15f18ee..00000000 --- a/vector/src/main/java/im/vector/riotredesign/core/utils/AnimationUtils.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 89d76bf7..30f5746e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -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(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(composerEditText) + Autocomplete.on(composerLayout.composerEditText) .with(commandAutocompletePolicy) .with(autocompleteCommandPresenter) .with(elevation) @@ -343,7 +331,7 @@ class RoomDetailFragment : .build() autocompleteUserPresenter.callback = this - Autocomplete.on(composerEditText) + Autocomplete.on(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) { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerView.kt new file mode 100644 index 00000000..c0fc2725 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/composer/TextComposerView.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 4fee2f41..8d32a7a4 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -67,6 +67,13 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel().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 + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + diff --git a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml index 9a354fc2..48048e62 100644 --- a/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml +++ b/vector/src/main/res/layout/constraint_set_composer_layout_expanded.xml @@ -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" /> + + @@ -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 @@ @@ -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" /> diff --git a/vector/src/main/res/layout/fragment_room_detail.xml b/vector/src/main/res/layout/fragment_room_detail.xml index 0f631722..970b7f4d 100644 --- a/vector/src/main/res/layout/fragment_room_detail.xml +++ b/vector/src/main/res/layout/fragment_room_detail.xml @@ -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" /> - - + - + tools:constraintSet="@layout/constraint_set_composer_layout_compact" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">