forked from GitHub-Mirror/riotX-android
Reactions: Display existing reactions below the message
- Reaction Button Bellow the message - Upgrade to constraint layout 2.0.0 beta (for Constraint Helpers / Flow) - Added tap on member name action - Cleaning
This commit is contained in:
parent
8929898397
commit
00fd866cc8
@ -135,7 +135,7 @@ dependencies {
|
|||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
|
implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta1'
|
||||||
implementation 'androidx.core:core-ktx:1.0.1'
|
implementation 'androidx.core:core-ktx:1.0.1'
|
||||||
|
|
||||||
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1'
|
implementation 'com.jakewharton.threetenabp:threetenabp:1.1.1'
|
||||||
|
@ -1,3 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.riotredesign.core.utils
|
package im.vector.riotredesign.core.utils
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.riotredesign.core.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.TypedValue
|
||||||
|
|
||||||
|
|
||||||
|
object DimensionUtils {
|
||||||
|
|
||||||
|
fun dpToPx(dp: Int, context: Context): Int {
|
||||||
|
return TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
dp.toFloat(),
|
||||||
|
context.resources.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,9 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail
|
package im.vector.riotredesign.features.home.room.detail
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
@ -25,9 +27,11 @@ import android.os.Bundle
|
|||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.text.Spannable
|
import android.text.Spannable
|
||||||
|
import android.text.TextUtils
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
@ -52,7 +56,6 @@ import im.vector.matrix.android.api.session.Session
|
|||||||
import im.vector.matrix.android.api.session.room.model.message.*
|
import im.vector.matrix.android.api.session.room.model.message.*
|
||||||
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.user.model.User
|
import im.vector.matrix.android.api.session.user.model.User
|
||||||
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
|
|
||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.dialogs.DialogListItem
|
import im.vector.riotredesign.core.dialogs.DialogListItem
|
||||||
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.riotredesign.core.epoxy.LayoutManagerStateRestorer
|
||||||
@ -82,6 +85,7 @@ import im.vector.riotredesign.features.media.ImageContentRenderer
|
|||||||
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
import im.vector.riotredesign.features.media.ImageMediaViewerActivity
|
||||||
import im.vector.riotredesign.features.media.VideoContentRenderer
|
import im.vector.riotredesign.features.media.VideoContentRenderer
|
||||||
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
|
import im.vector.riotredesign.features.media.VideoMediaViewerActivity
|
||||||
|
import im.vector.riotredesign.features.reactions.EmojiReactionPickerActivity
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
@ -115,6 +119,26 @@ class RoomDetailFragment :
|
|||||||
setArguments(args)
|
setArguments(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize the display name.
|
||||||
|
*
|
||||||
|
* @param displayName the display name to sanitize
|
||||||
|
* @return the sanitized display name
|
||||||
|
*/
|
||||||
|
fun sanitizeDisplayname(displayName: String): String? {
|
||||||
|
var displayName = displayName
|
||||||
|
// sanity checks
|
||||||
|
if (!TextUtils.isEmpty(displayName)) {
|
||||||
|
val ircPattern = " (IRC)"
|
||||||
|
|
||||||
|
if (displayName.endsWith(ircPattern)) {
|
||||||
|
displayName = displayName.substring(0, displayName.length - ircPattern.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return displayName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val session by inject<Session>()
|
private val session by inject<Session>()
|
||||||
@ -445,6 +469,11 @@ class RoomDetailFragment :
|
|||||||
override fun onAvatarClicked(informationData: MessageInformationData) {
|
override fun onAvatarClicked(informationData: MessageInformationData) {
|
||||||
vectorBaseActivity.notImplemented()
|
vectorBaseActivity.notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
override fun onMemberNameClicked(informationData: MessageInformationData) {
|
||||||
|
insertUserDisplayNameInTextEditor(informationData.memberName?.toString())
|
||||||
|
}
|
||||||
// AutocompleteUserPresenter.Callback
|
// AutocompleteUserPresenter.Callback
|
||||||
|
|
||||||
override fun onQueryUsers(query: CharSequence?) {
|
override fun onQueryUsers(query: CharSequence?) {
|
||||||
@ -514,4 +543,50 @@ class RoomDetailFragment :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//utils
|
||||||
|
/**
|
||||||
|
* Insert an user displayname in the message editor.
|
||||||
|
*
|
||||||
|
* @param text the text to insert.
|
||||||
|
*/
|
||||||
|
private fun insertUserDisplayNameInTextEditor(text: String?) {
|
||||||
|
if (null != text) {
|
||||||
|
// var vibrate = false
|
||||||
|
|
||||||
|
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)
|
||||||
|
// vibrate = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// another user
|
||||||
|
if (TextUtils.isEmpty(composerEditText.text)) {
|
||||||
|
// Ensure displayName will not be interpreted as a Slash command
|
||||||
|
if (text.startsWith("/")) {
|
||||||
|
composerEditText.append("\\")
|
||||||
|
}
|
||||||
|
composerEditText.append(sanitizeDisplayname(text)!! + ": ")
|
||||||
|
} else {
|
||||||
|
composerEditText.text.insert(composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// vibrate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (vibrate && PreferencesManager.vibrateWhenMentioning(context)) {
|
||||||
|
// val v= context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||||
|
// if (v?.hasVibrator() == true) {
|
||||||
|
// v.vibrate(100)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
composerEditText.requestFocus()
|
||||||
|
val imm = context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
imm?.showSoftInput(composerEditText, InputMethodManager.SHOW_FORCED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ class TimelineEventController(private val dateFormatter: TimelineDateFormatter,
|
|||||||
fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View)
|
fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View)
|
||||||
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
|
fun onEventLongClicked(informationData: MessageInformationData, messageContent: MessageContent, view: View): Boolean
|
||||||
fun onAvatarClicked(informationData: MessageInformationData)
|
fun onAvatarClicked(informationData: MessageInformationData)
|
||||||
|
fun onMemberNameClicked(informationData: MessageInformationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val collapsedEventIds = linkedSetOf<String>()
|
private val collapsedEventIds = linkedSetOf<String>()
|
||||||
|
@ -81,6 +81,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
memberName = formattedMemberName,
|
memberName = formattedMemberName,
|
||||||
showInformation = showInformation)
|
showInformation = showInformation)
|
||||||
|
|
||||||
|
//Test for reactions UX
|
||||||
|
//informationData.orderedReactionList = listOf( Triple("👍",1,false), Triple("👎",2,false))
|
||||||
|
|
||||||
// val all = event.root.toContent()
|
// val all = event.root.toContent()
|
||||||
// val ev = all.toModel<Event>()
|
// val ev = all.toModel<Event>()
|
||||||
return when (messageContent) {
|
return when (messageContent) {
|
||||||
@ -105,6 +108,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onAvatarClicked(informationData)
|
callback?.onAvatarClicked(informationData)
|
||||||
}))
|
}))
|
||||||
|
.memberClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onMemberNameClicked(informationData)
|
||||||
|
}))
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onEventCellClicked(informationData, messageContent, view)
|
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||||
@ -129,6 +136,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onAvatarClicked(informationData)
|
callback?.onAvatarClicked(informationData)
|
||||||
}))
|
}))
|
||||||
|
.memberClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onMemberNameClicked(informationData)
|
||||||
|
}))
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onEventCellClicked(informationData, messageContent, view)
|
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||||
@ -170,6 +181,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onAvatarClicked(informationData)
|
callback?.onAvatarClicked(informationData)
|
||||||
}))
|
}))
|
||||||
|
.memberClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onMemberNameClicked(informationData)
|
||||||
|
}))
|
||||||
.clickListener(
|
.clickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onImageMessageClicked(messageContent, data, view)
|
callback?.onImageMessageClicked(messageContent, data, view)
|
||||||
@ -212,6 +227,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onAvatarClicked(informationData)
|
callback?.onAvatarClicked(informationData)
|
||||||
}))
|
}))
|
||||||
|
.memberClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onMemberNameClicked(informationData)
|
||||||
|
}))
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onEventCellClicked(informationData, messageContent, view)
|
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||||
@ -239,6 +258,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onAvatarClicked(informationData)
|
callback?.onAvatarClicked(informationData)
|
||||||
}))
|
}))
|
||||||
|
.memberClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onMemberNameClicked(informationData)
|
||||||
|
}))
|
||||||
//click on the text
|
//click on the text
|
||||||
.clickListener(
|
.clickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
@ -272,6 +295,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onAvatarClicked(informationData)
|
callback?.onAvatarClicked(informationData)
|
||||||
}))
|
}))
|
||||||
|
.memberClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onMemberNameClicked(informationData)
|
||||||
|
}))
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onEventCellClicked(informationData, messageContent, view)
|
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||||
@ -296,6 +323,10 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onAvatarClicked(informationData)
|
callback?.onAvatarClicked(informationData)
|
||||||
}))
|
}))
|
||||||
|
.memberClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
|
callback?.onMemberNameClicked(informationData)
|
||||||
|
}))
|
||||||
.cellClickListener(
|
.cellClickListener(
|
||||||
DebouncedClickListener(View.OnClickListener { view ->
|
DebouncedClickListener(View.OnClickListener { view ->
|
||||||
callback?.onEventCellClicked(informationData, messageContent, view)
|
callback?.onEventCellClicked(informationData, messageContent, view)
|
||||||
|
@ -16,12 +16,20 @@
|
|||||||
|
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import im.vector.riotredesign.R
|
import androidx.constraintlayout.helper.widget.Flow
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||||
import im.vector.riotredesign.features.home.AvatarRenderer
|
import im.vector.riotredesign.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
|
||||||
|
|
||||||
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
||||||
@ -37,6 +45,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
@EpoxyAttribute
|
@EpoxyAttribute
|
||||||
var avatarClickListener: View.OnClickListener? = null
|
var avatarClickListener: View.OnClickListener? = null
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
var memberClickListener: View.OnClickListener? = null
|
||||||
|
|
||||||
override fun bind(holder: H) {
|
override fun bind(holder: H) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
if (informationData.showInformation) {
|
if (informationData.showInformation) {
|
||||||
@ -46,15 +57,17 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
height = size
|
height = size
|
||||||
width = size
|
width = size
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.avatarImageView.visibility = View.VISIBLE
|
holder.avatarImageView.visibility = View.VISIBLE
|
||||||
holder.avatarImageView.setOnClickListener(avatarClickListener)
|
holder.avatarImageView.setOnClickListener(avatarClickListener)
|
||||||
holder.memberNameView.visibility = View.VISIBLE
|
holder.memberNameView.visibility = View.VISIBLE
|
||||||
|
holder.memberNameView.setOnClickListener(memberClickListener)
|
||||||
holder.timeView.visibility = View.VISIBLE
|
holder.timeView.visibility = View.VISIBLE
|
||||||
holder.timeView.text = informationData.time
|
holder.timeView.text = informationData.time
|
||||||
holder.memberNameView.text = informationData.memberName
|
holder.memberNameView.text = informationData.memberName
|
||||||
AvatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView)
|
AvatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView)
|
||||||
} else {
|
} else {
|
||||||
|
holder.avatarImageView.setOnClickListener(null)
|
||||||
|
holder.memberNameView.setOnClickListener(null)
|
||||||
holder.avatarImageView.visibility = View.GONE
|
holder.avatarImageView.visibility = View.GONE
|
||||||
holder.memberNameView.visibility = View.GONE
|
holder.memberNameView.visibility = View.GONE
|
||||||
holder.timeView.visibility = View.GONE
|
holder.timeView.visibility = View.GONE
|
||||||
@ -62,6 +75,30 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
holder.view.setOnClickListener(cellClickListener)
|
holder.view.setOnClickListener(cellClickListener)
|
||||||
holder.view.setOnLongClickListener(longClickListener)
|
holder.view.setOnLongClickListener(longClickListener)
|
||||||
|
|
||||||
|
if (informationData.orderedReactionList.isNullOrEmpty()) {
|
||||||
|
holder.reactionWrapper.isVisible = false
|
||||||
|
} else {
|
||||||
|
holder.reactionWrapper.isVisible = true
|
||||||
|
//clear all reaction buttons (but not the Flow helper!)
|
||||||
|
holder.reactionWrapper.children.forEach { (it as? ReactionButton)?.isGone = true }
|
||||||
|
val idToRefInFlow = ArrayList<Int>()
|
||||||
|
informationData.orderedReactionList?.forEachIndexed { index, reaction ->
|
||||||
|
(holder.reactionWrapper.children.elementAt(index) as? ReactionButton)?.let { reactionButton ->
|
||||||
|
reactionButton.isVisible = true
|
||||||
|
idToRefInFlow.add(reactionButton.id)
|
||||||
|
reactionButton.reactionString = reaction.first
|
||||||
|
reactionButton.reactionCount = reaction.second
|
||||||
|
reactionButton.setChecked(reaction.third)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
|
||||||
|
// so have to update ref ids
|
||||||
|
holder.reactionFlowHelper.referencedIds = idToRefInFlow.toIntArray()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) {
|
||||||
|
holder.reactionFlowHelper.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun View.renderSendState() {
|
protected fun View.renderSendState() {
|
||||||
@ -74,6 +111,9 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
val avatarImageView by bind<ImageView>(R.id.messageAvatarImageView)
|
||||||
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
val memberNameView by bind<TextView>(R.id.messageMemberNameView)
|
||||||
val timeView by bind<TextView>(R.id.messageTimeView)
|
val timeView by bind<TextView>(R.id.messageTimeView)
|
||||||
|
|
||||||
|
val reactionWrapper: ViewGroup by bind(R.id.messageBottomInfo)
|
||||||
|
val reactionFlowHelper: Flow by bind(R.id.reactionsFlowHelper)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewStub
|
import android.view.ViewStub
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
@ -24,6 +22,7 @@ import androidx.constraintlayout.widget.Guideline
|
|||||||
import im.vector.riotredesign.R
|
import im.vector.riotredesign.R
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyHolder
|
||||||
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
import im.vector.riotredesign.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||||
|
|
||||||
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
|
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() {
|
||||||
|
|
||||||
@ -70,13 +69,5 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||||||
SMALL(30),
|
SMALL(30),
|
||||||
NONE(0)
|
NONE(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dpToPx(dp: Int, context: Context): Int {
|
|
||||||
return TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP,
|
|
||||||
dp.toFloat(),
|
|
||||||
context.resources.displayMetrics
|
|
||||||
).toInt()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,5 +29,7 @@ data class MessageInformationData(
|
|||||||
val time: CharSequence? = null,
|
val time: CharSequence? = null,
|
||||||
val avatarUrl: String?,
|
val avatarUrl: String?,
|
||||||
val memberName: CharSequence? = null,
|
val memberName: CharSequence? = null,
|
||||||
val showInformation: Boolean = true
|
val showInformation: Boolean = true,
|
||||||
|
/*List of reactions (emoji,count,isSelected)*/
|
||||||
|
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
@ -20,10 +20,12 @@ import android.media.ExifInterface
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.github.piasy.biv.view.BigImageView
|
import com.github.piasy.biv.view.BigImageView
|
||||||
import im.vector.matrix.android.api.Matrix
|
import im.vector.matrix.android.api.Matrix
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.riotredesign.core.glide.GlideApp
|
import im.vector.riotredesign.core.glide.GlideApp
|
||||||
|
import im.vector.riotredesign.core.utils.DimensionUtils.dpToPx
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@ -67,6 +69,7 @@ object ImageContentRenderer {
|
|||||||
.with(imageView)
|
.with(imageView)
|
||||||
.load(resolvedUrl)
|
.load(resolvedUrl)
|
||||||
.dontAnimate()
|
.dontAnimate()
|
||||||
|
.transform(RoundedCorners(dpToPx(8,imageView.context)))
|
||||||
.thumbnail(0.3f)
|
.thumbnail(0.3f)
|
||||||
.into(imageView)
|
.into(imageView)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.riotredesign.features.reactions.widget
|
||||||
|
|
||||||
|
import android.animation.ArgbEvaluator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.*
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Property
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This view is responsible for drawing big circle that will pulse when clicked
|
||||||
|
* As describe in http://frogermcs.github.io/twitters-like-animation-in-android-alternative/
|
||||||
|
*/
|
||||||
|
class CircleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
var startColor = -0xa8de
|
||||||
|
var endColor = -0x3ef9
|
||||||
|
|
||||||
|
private val argbEvaluator = ArgbEvaluator()
|
||||||
|
|
||||||
|
private val circlePaint = Paint()
|
||||||
|
private val maskPaint = Paint()
|
||||||
|
|
||||||
|
private var tempBitmap: Bitmap? = null
|
||||||
|
private var tempCanvas: Canvas? = null
|
||||||
|
|
||||||
|
var outerCircleRadiusProgress = 0f
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
updateCircleColor()
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
var innerCircleRadiusProgress = 0f
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var maxCircleSize: Int = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
circlePaint.style = Paint.Style.FILL
|
||||||
|
maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
maxCircleSize = w / 2
|
||||||
|
tempBitmap = Bitmap.createBitmap(width, width, Bitmap.Config.ARGB_8888)
|
||||||
|
tempCanvas = Canvas(tempBitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
tempCanvas!!.drawColor(0xffffff, PorterDuff.Mode.CLEAR)
|
||||||
|
tempCanvas!!.drawCircle(width / 2f, height / 2f, outerCircleRadiusProgress * maxCircleSize, circlePaint)
|
||||||
|
tempCanvas!!.drawCircle(width / 2f, height / 2f, innerCircleRadiusProgress * maxCircleSize, maskPaint)
|
||||||
|
canvas.drawBitmap(tempBitmap, 0f, 0f, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun setInnerCircleRadiusProgress(innerCircleRadiusProgress: Float) {
|
||||||
|
// this.innerCircleRadiusProgress = innerCircleRadiusProgress
|
||||||
|
// postInvalidate()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fun getInnerCircleRadiusProgress(): Float {
|
||||||
|
// return innerCircleRadiusProgress
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fun setOuterCircleRadiusProgress(outerCircleRadiusProgress: Float) {
|
||||||
|
// this.outerCircleRadiusProgress = outerCircleRadiusProgress
|
||||||
|
// updateCircleColor()
|
||||||
|
// postInvalidate()
|
||||||
|
// }
|
||||||
|
|
||||||
|
private fun updateCircleColor() {
|
||||||
|
var colorProgress = clamp(outerCircleRadiusProgress, 0.5f, 1f) as Float
|
||||||
|
colorProgress = mapValueFromRangeToRange(colorProgress, 0.5f, 1f, 0f, 1f)
|
||||||
|
this.circlePaint.color = argbEvaluator.evaluate(colorProgress, startColor, endColor) as Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun getOuterCircleRadiusProgress(): Float {
|
||||||
|
// return outerCircleRadiusProgress
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
val INNER_CIRCLE_RADIUS_PROGRESS: Property<CircleView, Float> = object : Property<CircleView, Float>(Float::class.java, "innerCircleRadiusProgress") {
|
||||||
|
override operator fun get(`object`: CircleView): Float? {
|
||||||
|
return `object`.innerCircleRadiusProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun set(`object`: CircleView, value: Float?) {
|
||||||
|
value?.let {
|
||||||
|
`object`.innerCircleRadiusProgress = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val OUTER_CIRCLE_RADIUS_PROGRESS: Property<CircleView, Float> = object : Property<CircleView, Float>(Float::class.java, "outerCircleRadiusProgress") {
|
||||||
|
override operator fun get(`object`: CircleView): Float? {
|
||||||
|
return `object`.outerCircleRadiusProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun set(`object`: CircleView, value: Float?) {
|
||||||
|
value?.let {
|
||||||
|
`object`.outerCircleRadiusProgress = it
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun mapValueFromRangeToRange(value: Float, fromLow: Float, fromHigh: Float, toLow: Float, toHigh: Float): Float {
|
||||||
|
return toLow + (value - fromLow) / (fromHigh - fromLow) * (toHigh - toLow)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clamp(value: Float, low: Float, high: Float): Float {
|
||||||
|
return Math.min(Math.max(value, low), high)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.riotredesign.features.reactions.widget
|
||||||
|
|
||||||
|
import android.animation.ArgbEvaluator
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.util.Property
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This view will draw dots floating around the center of it's view
|
||||||
|
* As describe in http://frogermcs.github.io/twitters-like-animation-in-android-alternative/
|
||||||
|
*/
|
||||||
|
class DotsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private var COLOR_1 = -0x3ef9
|
||||||
|
private var COLOR_2 = -0x6800
|
||||||
|
private var COLOR_3 = -0xa8de
|
||||||
|
private var COLOR_4 = -0xbbcca
|
||||||
|
|
||||||
|
private val circlePaints = arrayOfNulls<Paint>(4)
|
||||||
|
|
||||||
|
private var centerX: Int = 0
|
||||||
|
private var centerY: Int = 0
|
||||||
|
|
||||||
|
private var maxOuterDotsRadius: Float = 0.toFloat()
|
||||||
|
private var maxInnerDotsRadius: Float = 0.toFloat()
|
||||||
|
private var maxDotSize: Float = 0.toFloat()
|
||||||
|
|
||||||
|
var currentProgress = 0f
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
updateInnerDotsPosition()
|
||||||
|
updateOuterDotsPosition()
|
||||||
|
updateDotsPaints()
|
||||||
|
updateDotsAlpha()
|
||||||
|
|
||||||
|
postInvalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentRadius1 = 0f
|
||||||
|
private var currentDotSize1 = 0f
|
||||||
|
|
||||||
|
private var currentDotSize2 = 0f
|
||||||
|
private var currentRadius2 = 0f
|
||||||
|
|
||||||
|
private val argbEvaluator = ArgbEvaluator()
|
||||||
|
|
||||||
|
init {
|
||||||
|
for (i in circlePaints.indices) {
|
||||||
|
circlePaints[i] = Paint()
|
||||||
|
circlePaints[i]!!.style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh)
|
||||||
|
centerX = w / 2
|
||||||
|
centerY = h / 2
|
||||||
|
maxDotSize = 3f
|
||||||
|
maxOuterDotsRadius = w / 2 - maxDotSize * 2
|
||||||
|
maxInnerDotsRadius = 0.8f * maxOuterDotsRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
drawOuterDotsFrame(canvas)
|
||||||
|
drawInnerDotsFrame(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawOuterDotsFrame(canvas: Canvas) {
|
||||||
|
for (i in 0 until DOTS_COUNT) {
|
||||||
|
val cX = (centerX + currentRadius1 * Math.cos(i.toDouble() * OUTER_DOTS_POSITION_ANGLE.toDouble() * Math.PI / 180)).toFloat()
|
||||||
|
val cY = (centerY + currentRadius1 * Math.sin(i.toDouble() * OUTER_DOTS_POSITION_ANGLE.toDouble() * Math.PI / 180)).toFloat()
|
||||||
|
canvas.drawCircle(cX, cY, currentDotSize1, circlePaints[i % circlePaints.size])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawInnerDotsFrame(canvas: Canvas) {
|
||||||
|
for (i in 0 until DOTS_COUNT) {
|
||||||
|
val cX = (centerX + currentRadius2 * Math.cos((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)).toFloat()
|
||||||
|
val cY = (centerY + currentRadius2 * Math.sin((i * OUTER_DOTS_POSITION_ANGLE - 10) * Math.PI / 180)).toFloat()
|
||||||
|
canvas.drawCircle(cX, cY, currentDotSize2, circlePaints[(i + 1) % circlePaints.size])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun setCurrentProgress(currentProgress: Float) {
|
||||||
|
// this.currentProgress = currentProgress
|
||||||
|
//
|
||||||
|
// updateInnerDotsPosition()
|
||||||
|
// updateOuterDotsPosition()
|
||||||
|
// updateDotsPaints()
|
||||||
|
// updateDotsAlpha()
|
||||||
|
//
|
||||||
|
// postInvalidate()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fun getCurrentProgress(): Float {
|
||||||
|
// return currentProgress
|
||||||
|
// }
|
||||||
|
|
||||||
|
private fun updateInnerDotsPosition() {
|
||||||
|
if (currentProgress < 0.3f) {
|
||||||
|
this.currentRadius2 = CircleView.mapValueFromRangeToRange(currentProgress, 0f, 0.3f, 0f, maxInnerDotsRadius)
|
||||||
|
} else {
|
||||||
|
this.currentRadius2 = maxInnerDotsRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentProgress < 0.2) {
|
||||||
|
this.currentDotSize2 = maxDotSize
|
||||||
|
} else if (currentProgress < 0.5) {
|
||||||
|
this.currentDotSize2 = CircleView.mapValueFromRangeToRange(
|
||||||
|
currentProgress, 0.2f, 0.5f, maxDotSize, 0.3f * maxDotSize)
|
||||||
|
} else {
|
||||||
|
this.currentDotSize2 = CircleView.mapValueFromRangeToRange(
|
||||||
|
currentProgress, 0.5f, 1f, maxDotSize * 0.3f, 0f)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun setColors(primary: Int, secondary: Int) {
|
||||||
|
COLOR_1 = primary
|
||||||
|
COLOR_2 = secondary
|
||||||
|
COLOR_3 = primary
|
||||||
|
COLOR_4 = secondary
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateOuterDotsPosition() {
|
||||||
|
if (currentProgress < 0.3f) {
|
||||||
|
this.currentRadius1 = CircleView.mapValueFromRangeToRange(
|
||||||
|
currentProgress, 0.0f, 0.3f, 0f, maxOuterDotsRadius * 0.8f)
|
||||||
|
} else {
|
||||||
|
this.currentRadius1 = CircleView.mapValueFromRangeToRange(
|
||||||
|
currentProgress, 0.3f, 1f, 0.8f * maxOuterDotsRadius, maxOuterDotsRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentProgress < 0.7) {
|
||||||
|
this.currentDotSize1 = maxDotSize
|
||||||
|
} else {
|
||||||
|
this.currentDotSize1 = CircleView.mapValueFromRangeToRange(
|
||||||
|
currentProgress, 0.7f, 1f, maxDotSize, 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDotsPaints() {
|
||||||
|
if (currentProgress < 0.5f) {
|
||||||
|
val progress = CircleView.mapValueFromRangeToRange(currentProgress, 0f, 0.5f, 0f, 1f) as Float
|
||||||
|
circlePaints[0]?.color = argbEvaluator.evaluate(progress, COLOR_1, COLOR_2) as Int
|
||||||
|
circlePaints[1]?.color = argbEvaluator.evaluate(progress, COLOR_2, COLOR_3) as Int
|
||||||
|
circlePaints[2]?.color = argbEvaluator.evaluate(progress, COLOR_3, COLOR_4) as Int
|
||||||
|
circlePaints[3]?.color = argbEvaluator.evaluate(progress, COLOR_4, COLOR_1) as Int
|
||||||
|
} else {
|
||||||
|
val progress = CircleView.mapValueFromRangeToRange(currentProgress, 0.5f, 1f, 0f, 1f) as Float
|
||||||
|
circlePaints[0]?.color = argbEvaluator.evaluate(progress, COLOR_2, COLOR_3) as Int
|
||||||
|
circlePaints[1]?.color = argbEvaluator.evaluate(progress, COLOR_3, COLOR_4) as Int
|
||||||
|
circlePaints[2]?.color = argbEvaluator.evaluate(progress, COLOR_4, COLOR_1) as Int
|
||||||
|
circlePaints[3]?.color = argbEvaluator.evaluate(progress, COLOR_1, COLOR_2) as Int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDotsAlpha() {
|
||||||
|
val progress = CircleView.clamp(currentProgress, 0.6f, 1f) as Float
|
||||||
|
val alpha = (CircleView.mapValueFromRangeToRange(progress, 0.6f, 1f, 255f, 0f) as? Float)?.toInt()
|
||||||
|
?: 0
|
||||||
|
circlePaints.forEach { it?.alpha = alpha }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val DOTS_COUNT = 7
|
||||||
|
private val OUTER_DOTS_POSITION_ANGLE = 360 / DOTS_COUNT
|
||||||
|
|
||||||
|
val DOTS_PROGRESS: Property<DotsView, Float> = object : Property<DotsView, Float>(Float::class.java, "dotsProgress") {
|
||||||
|
override operator fun get(`object`: DotsView): Float? {
|
||||||
|
return `object`.currentProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
override operator fun set(`object`: DotsView, value: Float?) {
|
||||||
|
`object`.currentProgress = value!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,331 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019 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.riotredesign.features.reactions.widget
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorListenerAdapter
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import android.view.animation.OvershootInterpolator
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.annotation.ColorRes
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import im.vector.riotredesign.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An animated reaction button.
|
||||||
|
* Displays a String reaction (emoji), with a count, and that can be selected or not (toggle)
|
||||||
|
*/
|
||||||
|
class ReactionButton @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr), View.OnClickListener {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DECCELERATE_INTERPOLATOR = DecelerateInterpolator()
|
||||||
|
private val ACCELERATE_DECELERATE_INTERPOLATOR = AccelerateDecelerateInterpolator()
|
||||||
|
private val OVERSHOOT_INTERPOLATOR = OvershootInterpolator(4f)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private var emojiView: TextView? = null
|
||||||
|
private var countTextView: TextView? = null
|
||||||
|
|
||||||
|
private var reactionSelector: View? = null
|
||||||
|
|
||||||
|
|
||||||
|
private var dotsView: DotsView
|
||||||
|
private var circleView: CircleView
|
||||||
|
var reactedListener: ReactedListener? = null
|
||||||
|
private var dotPrimaryColor: Int = 0
|
||||||
|
private var dotSecondaryColor: Int = 0
|
||||||
|
private var circleStartColor: Int = 0
|
||||||
|
private var circleEndColor: Int = 0
|
||||||
|
|
||||||
|
var reactionCount = 11
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
countTextView?.text = value.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var reactionString = "😀"
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
emojiView?.text = field
|
||||||
|
}
|
||||||
|
|
||||||
|
private var animationScaleFactor: Float = 0.toFloat()
|
||||||
|
|
||||||
|
private var isChecked: Boolean = false
|
||||||
|
|
||||||
|
private var animatorSet: AnimatorSet? = null
|
||||||
|
|
||||||
|
private var onDrawable: Drawable? = null
|
||||||
|
private var offDrawable: Drawable? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
LayoutInflater.from(getContext()).inflate(R.layout.reaction_button, this, true)
|
||||||
|
emojiView = findViewById(R.id.reactionText)
|
||||||
|
dotsView = findViewById(R.id.dots)
|
||||||
|
circleView = findViewById(R.id.circle)
|
||||||
|
reactionSelector = findViewById(R.id.reactionSelector)
|
||||||
|
countTextView = findViewById(R.id.reactionCount)
|
||||||
|
|
||||||
|
countTextView?.text = reactionCount.toString()
|
||||||
|
|
||||||
|
val array = context.obtainStyledAttributes(attrs, R.styleable.ReactionButton, defStyleAttr, 0)
|
||||||
|
|
||||||
|
onDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape)
|
||||||
|
offDrawable = ContextCompat.getDrawable(context, R.drawable.rounded_rect_shape_off)
|
||||||
|
|
||||||
|
circleStartColor = array.getColor(R.styleable.ReactionButton_circle_start_color, 0)
|
||||||
|
|
||||||
|
if (circleStartColor != 0)
|
||||||
|
circleView.startColor = circleStartColor
|
||||||
|
|
||||||
|
circleEndColor = array.getColor(R.styleable.ReactionButton_circle_end_color, 0)
|
||||||
|
|
||||||
|
if (circleEndColor != 0)
|
||||||
|
circleView.endColor = circleEndColor
|
||||||
|
|
||||||
|
dotPrimaryColor = array.getColor(R.styleable.ReactionButton_dots_primary_color, 0)
|
||||||
|
dotSecondaryColor = array.getColor(R.styleable.ReactionButton_dots_secondary_color, 0)
|
||||||
|
|
||||||
|
if (dotPrimaryColor != 0 && dotSecondaryColor != 0) {
|
||||||
|
dotsView.setColors(dotPrimaryColor, dotSecondaryColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
array.getString(R.styleable.ReactionButton_emoji)?.let {
|
||||||
|
reactionString = it
|
||||||
|
}
|
||||||
|
|
||||||
|
reactionCount = array.getInt(R.styleable.ReactionButton_reaction_count, 0)
|
||||||
|
|
||||||
|
val status = array.getBoolean(R.styleable.ReactionButton_toggled, false)
|
||||||
|
setChecked(status)
|
||||||
|
setOnClickListener(this)
|
||||||
|
array.recycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getDrawableFromResource(array: TypedArray, styleableIndexId: Int): Drawable? {
|
||||||
|
val id = array.getResourceId(styleableIndexId, -1)
|
||||||
|
|
||||||
|
return if (-1 != id) ContextCompat.getDrawable(context, id) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This triggers the entire functionality of the button such as icon changes,
|
||||||
|
* animations, listeners etc.
|
||||||
|
*
|
||||||
|
* @param v
|
||||||
|
*/
|
||||||
|
override fun onClick(v: View) {
|
||||||
|
|
||||||
|
if (!isEnabled)
|
||||||
|
return
|
||||||
|
|
||||||
|
isChecked = !isChecked
|
||||||
|
|
||||||
|
//icon!!.setImageDrawable(if (isChecked) likeDrawable else unLikeDrawable)
|
||||||
|
reactionSelector?.background = if (isChecked) onDrawable else offDrawable
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
reactedListener?.onReacted(this)
|
||||||
|
} else {
|
||||||
|
reactedListener?.onUnReacted(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (animatorSet != null) {
|
||||||
|
animatorSet!!.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isChecked) {
|
||||||
|
emojiView!!.animate().cancel()
|
||||||
|
emojiView!!.scaleX = 0f
|
||||||
|
emojiView!!.scaleY = 0f
|
||||||
|
|
||||||
|
circleView.innerCircleRadiusProgress = 0f
|
||||||
|
circleView.outerCircleRadiusProgress = 0f
|
||||||
|
dotsView.currentProgress = 0f
|
||||||
|
|
||||||
|
animatorSet = AnimatorSet()
|
||||||
|
|
||||||
|
val outerCircleAnimator = ObjectAnimator.ofFloat(circleView, CircleView.OUTER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f)
|
||||||
|
outerCircleAnimator.duration = 250
|
||||||
|
outerCircleAnimator.interpolator = DECCELERATE_INTERPOLATOR
|
||||||
|
|
||||||
|
val innerCircleAnimator = ObjectAnimator.ofFloat(circleView, CircleView.INNER_CIRCLE_RADIUS_PROGRESS, 0.1f, 1f)
|
||||||
|
innerCircleAnimator.duration = 200
|
||||||
|
innerCircleAnimator.startDelay = 200
|
||||||
|
innerCircleAnimator.interpolator = DECCELERATE_INTERPOLATOR
|
||||||
|
|
||||||
|
val starScaleYAnimator = ObjectAnimator.ofFloat(emojiView, ImageView.SCALE_Y, 0.2f, 1f)
|
||||||
|
starScaleYAnimator.duration = 350
|
||||||
|
starScaleYAnimator.startDelay = 250
|
||||||
|
starScaleYAnimator.interpolator = OVERSHOOT_INTERPOLATOR
|
||||||
|
|
||||||
|
val starScaleXAnimator = ObjectAnimator.ofFloat(emojiView, ImageView.SCALE_X, 0.2f, 1f)
|
||||||
|
starScaleXAnimator.duration = 350
|
||||||
|
starScaleXAnimator.startDelay = 250
|
||||||
|
starScaleXAnimator.interpolator = OVERSHOOT_INTERPOLATOR
|
||||||
|
|
||||||
|
val dotsAnimator = ObjectAnimator.ofFloat(dotsView, DotsView.DOTS_PROGRESS, 0f, 1f)//.ofFloat<DotsView>(dotsView, DotsView.DOTS_PROGRESS, 0, 1f)
|
||||||
|
dotsAnimator.duration = 900
|
||||||
|
dotsAnimator.startDelay = 50
|
||||||
|
dotsAnimator.interpolator = ACCELERATE_DECELERATE_INTERPOLATOR
|
||||||
|
|
||||||
|
animatorSet!!.playTogether(
|
||||||
|
outerCircleAnimator,
|
||||||
|
innerCircleAnimator,
|
||||||
|
starScaleYAnimator,
|
||||||
|
starScaleXAnimator,
|
||||||
|
dotsAnimator
|
||||||
|
)
|
||||||
|
|
||||||
|
animatorSet!!.addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationCancel(animation: Animator) {
|
||||||
|
circleView.innerCircleRadiusProgress = 0f
|
||||||
|
circleView.outerCircleRadiusProgress = 0f
|
||||||
|
dotsView.currentProgress = 0f
|
||||||
|
emojiView!!.scaleX = 1f
|
||||||
|
emojiView!!.scaleY = 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation: Animator) {
|
||||||
|
// if (animationEndListener != null) {
|
||||||
|
// // animationEndListener!!.onAnimationEnd(this@ReactionButton)
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
animatorSet!!.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to trigger the scale animation that takes places on the
|
||||||
|
* icon when the button is touched.
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
if (!isEnabled)
|
||||||
|
return true
|
||||||
|
|
||||||
|
when (event.action) {
|
||||||
|
MotionEvent.ACTION_DOWN ->
|
||||||
|
/*
|
||||||
|
Commented out this line and moved the animation effect to the action up event due to
|
||||||
|
conflicts that were occurring when library is used in sliding type views.
|
||||||
|
|
||||||
|
icon.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).setInterpolator(DECCELERATE_INTERPOLATOR);
|
||||||
|
*/
|
||||||
|
isPressed = true
|
||||||
|
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
val x = event.x
|
||||||
|
val y = event.y
|
||||||
|
val isInside = x > 0 && x < width && y > 0 && y < height
|
||||||
|
if (isPressed != isInside) {
|
||||||
|
isPressed = isInside
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionEvent.ACTION_UP -> {
|
||||||
|
emojiView!!.animate().scaleX(0.7f).scaleY(0.7f).setDuration(150).interpolator = DECCELERATE_INTERPOLATOR
|
||||||
|
emojiView!!.animate().scaleX(1f).scaleY(1f).interpolator = DECCELERATE_INTERPOLATOR
|
||||||
|
if (isPressed) {
|
||||||
|
performClick()
|
||||||
|
isPressed = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_CANCEL -> isPressed = false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This set sets the colours that are used for the little dots
|
||||||
|
* that will be exploding once the like button is clicked.
|
||||||
|
*
|
||||||
|
* @param primaryColor
|
||||||
|
* @param secondaryColor
|
||||||
|
*/
|
||||||
|
fun setExplodingDotColorsRes(@ColorRes primaryColor: Int, @ColorRes secondaryColor: Int) {
|
||||||
|
dotsView.setColors(ContextCompat.getColor(context, primaryColor), ContextCompat.getColor(context, secondaryColor))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setExplodingDotColorsInt(@ColorInt primaryColor: Int, @ColorInt secondaryColor: Int) {
|
||||||
|
dotsView.setColors(primaryColor, secondaryColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCircleStartColorRes(@ColorRes circleStartColor: Int) {
|
||||||
|
this.circleStartColor = ContextCompat.getColor(context, circleStartColor)
|
||||||
|
circleView.startColor = this.circleStartColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCircleStartColorInt(@ColorInt circleStartColor: Int) {
|
||||||
|
this.circleStartColor = circleStartColor
|
||||||
|
circleView.startColor = circleStartColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCircleEndColorRes(@ColorRes circleEndColor: Int) {
|
||||||
|
this.circleEndColor = ContextCompat.getColor(context, circleEndColor)
|
||||||
|
circleView.endColor = this.circleEndColor
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the initial state of the button to liked
|
||||||
|
* or unliked.
|
||||||
|
*
|
||||||
|
* @param status
|
||||||
|
*/
|
||||||
|
fun setChecked(status: Boolean?) {
|
||||||
|
if (status!!) {
|
||||||
|
isChecked = true
|
||||||
|
reactionSelector?.background = onDrawable
|
||||||
|
} else {
|
||||||
|
isChecked = false
|
||||||
|
reactionSelector?.background = offDrawable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the factor by which the dots should be sized.
|
||||||
|
*/
|
||||||
|
fun setAnimationScaleFactor(animationScaleFactor: Float) {
|
||||||
|
this.animationScaleFactor = animationScaleFactor
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface ReactedListener {
|
||||||
|
fun onReacted(reactionButton: ReactionButton)
|
||||||
|
fun onUnReacted(reactionButton: ReactionButton)
|
||||||
|
}
|
||||||
|
}
|
13
vector/src/main/res/drawable/rounded_rect_shape.xml
Normal file
13
vector/src/main/res/drawable/rounded_rect_shape.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<size android:width="40dp" android:height="22dp"/>
|
||||||
|
|
||||||
|
<solid android:color="@color/light_blue_grey" />
|
||||||
|
|
||||||
|
<stroke android:width="1dp" android:color="@color/accent_color_light" />
|
||||||
|
|
||||||
|
<corners android:radius="20dp" />
|
||||||
|
|
||||||
|
</shape>
|
13
vector/src/main/res/drawable/rounded_rect_shape_off.xml
Normal file
13
vector/src/main/res/drawable/rounded_rect_shape_off.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<size android:width="40dp" android:height="22dp"/>
|
||||||
|
|
||||||
|
<solid android:color="@color/light_blue_grey" />
|
||||||
|
|
||||||
|
<stroke android:width="1dp" android:color="@color/list_divider_color_light" />
|
||||||
|
|
||||||
|
<corners android:radius="20dp" />
|
||||||
|
|
||||||
|
</shape>
|
@ -9,7 +9,6 @@
|
|||||||
android:paddingLeft="8dp"
|
android:paddingLeft="8dp"
|
||||||
android:paddingRight="8dp">
|
android:paddingRight="8dp">
|
||||||
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/messageAvatarImageView"
|
android:id="@+id/messageAvatarImageView"
|
||||||
android:layout_width="44dp"
|
android:layout_width="44dp"
|
||||||
@ -28,7 +27,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/messageMemberNameView"
|
android:id="@+id/messageMemberNameView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
@ -39,8 +38,10 @@
|
|||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
|
app:layout_constraintEnd_toStartOf="@+id/messageTimeView"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||||
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="@tools:sample/full_names" />
|
tools:text="@tools:sample/full_names" />
|
||||||
@ -48,14 +49,15 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/messageTimeView"
|
android:id="@+id/messageTimeView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_marginLeft="8dp"
|
android:layout_marginLeft="8dp"
|
||||||
android:textColor="@color/brown_grey"
|
android:textColor="@color/brown_grey"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@id/messageMemberNameView"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintStart_toEndOf="@id/messageMemberNameView"
|
||||||
app:layout_constraintTop_toTopOf="@id/messageMemberNameView"
|
|
||||||
tools:text="@tools:sample/date/hhmm" />
|
tools:text="@tools:sample/date/hhmm" />
|
||||||
|
|
||||||
|
|
||||||
@ -80,4 +82,116 @@
|
|||||||
tools:ignore="MissingConstraints" />
|
tools:ignore="MissingConstraints" />
|
||||||
|
|
||||||
|
|
||||||
|
<!-- TODO: For now we show 8 reactions maximum, this will need rework when needed-->
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/messageBottomInfo"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/messageStartGuideline"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="👍"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:reaction_count="3"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="👎"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:reaction_count="10"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="😀"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction4"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="☹️"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction5"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="😱"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction6"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="❌"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction7"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="✔️"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.ReactionButton
|
||||||
|
android:id="@+id/messageBottomReaction8"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:emoji="♥️"
|
||||||
|
tools:ignore="MissingConstraints"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/reactionsFlowHelper"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="2dp"
|
||||||
|
app:constraint_referenced_ids="messageBottomReaction1,messageBottomReaction2,messageBottomReaction3,messageBottomReaction4,messageBottomReaction5,messageBottomReaction6,messageBottomReaction7,messageBottomReaction8"
|
||||||
|
app:flow_horizontalBias="0"
|
||||||
|
app:flow_horizontalGap="8dp"
|
||||||
|
app:flow_horizontalStyle="packed"
|
||||||
|
app:flow_verticalBias="0"
|
||||||
|
app:flow_verticalGap="4dp"
|
||||||
|
app:flow_wrapMode="chain"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
66
vector/src/main/res/layout/reaction_button.xml
Normal file
66
vector/src/main/res/layout/reaction_button.xml
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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:clipChildren="false"
|
||||||
|
android:layout_width="44dp"
|
||||||
|
android:layout_height="26dp">
|
||||||
|
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/reactionSelector"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/rounded_rect_shape" />
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.DotsView
|
||||||
|
android:id="@+id/dots"
|
||||||
|
android:layout_width="30dp"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/reactionText"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/reactionText"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/reactionText"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/reactionText"/>
|
||||||
|
|
||||||
|
<im.vector.riotredesign.features.reactions.widget.CircleView
|
||||||
|
android:id="@+id/circle"
|
||||||
|
android:layout_width="14dp"
|
||||||
|
android:layout_height="14dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/reactionText"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/reactionText"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/reactionText"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/reactionText"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reactionText"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="6dp"
|
||||||
|
android:layout_marginLeft="6dp"
|
||||||
|
android:gravity="center"
|
||||||
|
tools:text="👍"
|
||||||
|
android:textSize="13sp"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/reactionCount"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginEnd="6dp"
|
||||||
|
android:layout_marginRight="6dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:autoSizeMaxTextSize="14sp"
|
||||||
|
app:autoSizeMinTextSize="8sp"
|
||||||
|
app:autoSizeTextType="uniform"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
tools:text="10" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -266,5 +266,6 @@
|
|||||||
|
|
||||||
<style name="TimelineContentStubLayoutParams" parent="TimelineContentStubBaseParams">
|
<style name="TimelineContentStubLayoutParams" parent="TimelineContentStubBaseParams">
|
||||||
<item name="layout_constraintTop_toBottomOf">@id/messageMemberNameView</item>
|
<item name="layout_constraintTop_toBottomOf">@id/messageMemberNameView</item>
|
||||||
|
<item name="layout_constraintBottom_toTopOf">@id/messageBottomInfo</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
13
vector/src/main/res/values/values.xml
Normal file
13
vector/src/main/res/values/values.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<declare-styleable name="ReactionButton">
|
||||||
|
<attr name="dots_primary_color" format="color|reference" />
|
||||||
|
<attr name="dots_secondary_color" format="color|reference" />
|
||||||
|
<attr name="circle_start_color" format="color|reference" />
|
||||||
|
<attr name="circle_end_color" format="color|reference" />
|
||||||
|
<attr name="toggled" format="boolean" />
|
||||||
|
<attr name="emoji" format="string"/>
|
||||||
|
<attr name="reaction_count" format="integer"/>
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
Loading…
Reference in New Issue
Block a user