mirror of
https://github.com/nextcloud/talk-android.git
synced 2024-11-27 17:08:34 +03:00
add animated emoji reactions to calls (no signaling yet)
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
parent
2ce57f4956
commit
c379630610
6 changed files with 300 additions and 17 deletions
|
@ -66,6 +66,7 @@ import com.nextcloud.talk.application.NextcloudTalkApplication;
|
|||
import com.nextcloud.talk.call.CallParticipant;
|
||||
import com.nextcloud.talk.call.CallParticipantList;
|
||||
import com.nextcloud.talk.call.CallParticipantModel;
|
||||
import com.nextcloud.talk.call.ReactionAnimator;
|
||||
import com.nextcloud.talk.chat.ChatActivity;
|
||||
import com.nextcloud.talk.data.user.model.User;
|
||||
import com.nextcloud.talk.databinding.CallActivityBinding;
|
||||
|
@ -251,7 +252,7 @@ public class CallActivity extends CallBaseActivity {
|
|||
private List<PeerConnection.IceServer> iceServers;
|
||||
private CameraEnumerator cameraEnumerator;
|
||||
private String roomToken;
|
||||
private User conversationUser;
|
||||
public User conversationUser;
|
||||
private String conversationName;
|
||||
private String callSession;
|
||||
private MediaStream localStream;
|
||||
|
@ -370,6 +371,8 @@ public class CallActivity extends CallBaseActivity {
|
|||
|
||||
private boolean isModerator;
|
||||
|
||||
private ReactionAnimator reactionAnimator;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -506,6 +509,8 @@ public class CallActivity extends CallBaseActivity {
|
|||
initiateCall();
|
||||
}
|
||||
updateSelfVideoViewPosition();
|
||||
|
||||
reactionAnimator = new ReactionAnimator(context, binding.reactionAnimationWrapper, viewThemeUtils);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2725,6 +2730,10 @@ public class CallActivity extends CallBaseActivity {
|
|||
}
|
||||
}
|
||||
|
||||
public void addCallReaction(String emoji, String displayName) {
|
||||
reactionAnimator.addReaction(emoji, displayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary implementation of SignalingMessageReceiver until signaling related code is extracted from
|
||||
* CallActivity.
|
||||
|
|
184
app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt
Normal file
184
app/src/main/java/com/nextcloud/talk/call/ReactionAnimator.kt
Normal file
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Nextcloud Talk application
|
||||
*
|
||||
* @author Marcel Hibbe
|
||||
* Copyright (C) 2023 Marcel Hibbe <dev@mhibbe.de>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.nextcloud.talk.call
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.vanniktech.emoji.EmojiTextView
|
||||
|
||||
class ReactionAnimator(
|
||||
val context: Context,
|
||||
private val startPointView: RelativeLayout,
|
||||
val viewThemeUtils: ViewThemeUtils?
|
||||
) {
|
||||
private val reactionsList: MutableList<CallReaction> = ArrayList()
|
||||
|
||||
fun addReaction(
|
||||
emoji: String,
|
||||
displayName: String
|
||||
) {
|
||||
val callReaction = CallReaction(emoji, displayName)
|
||||
reactionsList.add(callReaction)
|
||||
|
||||
if (reactionsList.size == 1) {
|
||||
animateReaction(reactionsList[0])
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateReaction(
|
||||
callReaction: CallReaction
|
||||
) {
|
||||
val reactionWrapper = getReactionWrapperView(callReaction)
|
||||
|
||||
val params = RelativeLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
leftMargin = 0
|
||||
bottomMargin = 0
|
||||
}
|
||||
|
||||
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 1)
|
||||
startPointView.addView(reactionWrapper, params)
|
||||
|
||||
val moveWithFullAlpha = ObjectAnimator.ofFloat(
|
||||
reactionWrapper,
|
||||
TRANSLATION_Y_PROPERTY,
|
||||
POSITION_Y_WITH_FULL_ALPHA
|
||||
)
|
||||
moveWithFullAlpha.duration = DURATION_FULL_ALPHA
|
||||
moveWithFullAlpha.interpolator = LinearInterpolator()
|
||||
|
||||
val moveWithDecreasingAlpha = ObjectAnimator.ofFloat(
|
||||
reactionWrapper,
|
||||
TRANSLATION_Y_PROPERTY,
|
||||
POSITION_Y_WITH_DECREASING_ALPHA
|
||||
)
|
||||
moveWithDecreasingAlpha.duration = DURATION_DECREASING_ALPHA
|
||||
moveWithDecreasingAlpha.interpolator = LinearInterpolator()
|
||||
|
||||
val decreasingAlpha: ObjectAnimator = ObjectAnimator.ofFloat(
|
||||
reactionWrapper,
|
||||
ALPHA_PROPERTY,
|
||||
ZERO_ALPHA
|
||||
)
|
||||
decreasingAlpha.duration = DURATION_DECREASING_ALPHA
|
||||
|
||||
val animatorWithFullAlpha = AnimatorSet()
|
||||
animatorWithFullAlpha.play(moveWithFullAlpha)
|
||||
|
||||
animatorWithFullAlpha.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
reactionsList.remove(callReaction)
|
||||
if (reactionsList.isNotEmpty()) {
|
||||
animateReaction(reactionsList[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val animatorWithDecreasingAlpha = AnimatorSet()
|
||||
animatorWithDecreasingAlpha.playTogether(moveWithDecreasingAlpha, decreasingAlpha)
|
||||
|
||||
val finalAnimator = AnimatorSet()
|
||||
finalAnimator.play(animatorWithFullAlpha).before(animatorWithDecreasingAlpha)
|
||||
|
||||
finalAnimator.start()
|
||||
}
|
||||
|
||||
private fun getReactionWrapperView(callReaction: CallReaction): LinearLayout {
|
||||
val reactionWrapper = LinearLayout(context)
|
||||
reactionWrapper.orientation = LinearLayout.HORIZONTAL
|
||||
|
||||
val emojiView = EmojiTextView(context)
|
||||
emojiView.text = callReaction.emoji
|
||||
emojiView.textSize = 20f
|
||||
|
||||
val nameView = getNameView(callReaction)
|
||||
reactionWrapper.addView(emojiView)
|
||||
reactionWrapper.addView(nameView)
|
||||
return reactionWrapper
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun getNameView(callReaction: CallReaction): TextView {
|
||||
val nameView = TextView(context)
|
||||
|
||||
val nameViewParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
|
||||
nameViewParams.setMargins(20, 0, 20, 5)
|
||||
nameView.layoutParams = nameViewParams
|
||||
|
||||
nameView.text = " " + callReaction.userName + " "
|
||||
nameView.setTextColor(context.resources.getColor(R.color.white))
|
||||
|
||||
val backgroundColor = ContextCompat.getColor(
|
||||
context,
|
||||
R.color.colorPrimary
|
||||
)
|
||||
|
||||
val drawable = AppCompatResources
|
||||
.getDrawable(context, R.drawable.reaction_self_background)!!
|
||||
.mutate()
|
||||
DrawableCompat.setTintList(
|
||||
drawable,
|
||||
ColorStateList.valueOf(backgroundColor)
|
||||
)
|
||||
nameView.background = drawable
|
||||
return nameView
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TRANSLATION_Y_PROPERTY = "translationY"
|
||||
|
||||
// 1333ms to move emoji up 400px with full alpha
|
||||
private const val DURATION_FULL_ALPHA = 1333L
|
||||
private const val POSITION_Y_WITH_FULL_ALPHA = -400f
|
||||
|
||||
// 666ms to move emoji up 200px while decreasing alpha
|
||||
private const val DURATION_DECREASING_ALPHA = 666L
|
||||
private const val POSITION_Y_WITH_DECREASING_ALPHA = -600f
|
||||
|
||||
private const val ZERO_ALPHA = 0f
|
||||
private const val ALPHA_PROPERTY = "alpha"
|
||||
}
|
||||
}
|
||||
data class CallReaction(
|
||||
var emoji: String,
|
||||
var userName: String
|
||||
)
|
|
@ -24,6 +24,7 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import autodagger.AutoInjector
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
|
@ -34,7 +35,9 @@ import com.nextcloud.talk.application.NextcloudTalkApplication
|
|||
import com.nextcloud.talk.databinding.DialogMoreCallActionsBinding
|
||||
import com.nextcloud.talk.raisehand.viewmodel.RaiseHandViewModel
|
||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
|
||||
import com.nextcloud.talk.viewmodels.CallRecordingViewModel
|
||||
import com.vanniktech.emoji.EmojiTextView
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
|
@ -56,6 +59,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
|
|||
viewThemeUtils.platform.themeDialogDark(binding.root)
|
||||
|
||||
initItemsVisibility()
|
||||
initEmojiBar()
|
||||
initClickListeners()
|
||||
initObservers()
|
||||
}
|
||||
|
@ -68,6 +72,12 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
|
|||
}
|
||||
|
||||
private fun initItemsVisibility() {
|
||||
if (CapabilitiesUtilNew.isCallReactionsSupported(callActivity.conversationUser)) {
|
||||
binding.callEmojiBar.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.callEmojiBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (callActivity.isAllowedToStartOrStopRecording) {
|
||||
binding.recordCall.visibility = View.VISIBLE
|
||||
} else {
|
||||
|
@ -91,6 +101,40 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
|
|||
}
|
||||
}
|
||||
|
||||
private fun initEmojiBar() {
|
||||
if (CapabilitiesUtilNew.isCallReactionsSupported(callActivity.conversationUser)) {
|
||||
binding.advancedCallOptionsTitle.visibility = View.GONE
|
||||
|
||||
val capabilities = callActivity.conversationUser.capabilities
|
||||
val availableReactions: ArrayList<*> =
|
||||
capabilities?.spreedCapability?.config!!["call"]!!["supported-reactions"] as ArrayList<*>
|
||||
|
||||
val param = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
1.0f
|
||||
)
|
||||
|
||||
availableReactions.forEach {
|
||||
val emojiView = EmojiTextView(context)
|
||||
emojiView.text = it.toString()
|
||||
emojiView.textSize = 20f
|
||||
emojiView.layoutParams = param
|
||||
|
||||
emojiView.setOnClickListener { view ->
|
||||
callActivity.addCallReaction(
|
||||
(view as EmojiTextView).text.toString(),
|
||||
callActivity.conversationUser.displayName
|
||||
)
|
||||
dismiss()
|
||||
}
|
||||
binding.callEmojiBar.addView(emojiView)
|
||||
}
|
||||
} else {
|
||||
binding.callEmojiBar.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun initObservers() {
|
||||
callActivity.callRecordingViewModel.viewState.observe(this) { state ->
|
||||
when (state) {
|
||||
|
@ -102,12 +146,14 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
|
|||
)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
is CallRecordingViewModel.RecordingStartingState -> {
|
||||
binding.recordCallText.text = context.getText(R.string.record_cancel_start)
|
||||
binding.recordCallIcon.setImageDrawable(
|
||||
ContextCompat.getDrawable(context, R.drawable.record_stop)
|
||||
)
|
||||
}
|
||||
|
||||
is CallRecordingViewModel.RecordingStartedState -> {
|
||||
binding.recordCallText.text = context.getText(R.string.record_stop_description)
|
||||
binding.recordCallIcon.setImageDrawable(
|
||||
|
@ -115,12 +161,15 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
|
|||
)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
is CallRecordingViewModel.RecordingStoppingState -> {
|
||||
binding.recordCallText.text = context.getText(R.string.record_stopping)
|
||||
}
|
||||
|
||||
is CallRecordingViewModel.RecordingConfirmStopState -> {
|
||||
binding.recordCallText.text = context.getText(R.string.record_stop_description)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.e(TAG, "unknown viewState for callRecordingViewModel")
|
||||
}
|
||||
|
@ -136,6 +185,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
|
|||
)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
is RaiseHandViewModel.LoweredHandState -> {
|
||||
binding.raiseHandText.text = context.getText(R.string.raise_hand)
|
||||
binding.raiseHandIcon.setImageDrawable(
|
||||
|
@ -143,6 +193,7 @@ class MoreCallActionsDialog(private val callActivity: CallActivity) : BottomShee
|
|||
)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -174,6 +174,16 @@ object CapabilitiesUtilNew {
|
|||
return false
|
||||
}
|
||||
|
||||
fun isCallReactionsSupported(user: User?): Boolean {
|
||||
if (user?.capabilities != null) {
|
||||
val capabilities = user.capabilities
|
||||
return capabilities?.spreedCapability?.config?.containsKey("call") == true &&
|
||||
capabilities.spreedCapability!!.config!!["call"] != null &&
|
||||
capabilities.spreedCapability!!.config!!["call"]!!.containsKey("supported-reactions")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isUnifiedSearchAvailable(user: User): Boolean {
|
||||
return hasSpreedFeatureCapability(user, "unified-search")
|
||||
|
|
|
@ -175,22 +175,37 @@
|
|||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lower_hand_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
android:layout_marginBottom="@dimen/standard_half_margin"
|
||||
android:contentDescription="@string/lower_hand"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/call_buttons_background"
|
||||
app:borderWidth="0dp"
|
||||
app:fabCustomSize="40dp"
|
||||
app:shapeAppearance="@style/fab_3_rounded"
|
||||
app:srcCompat="@drawable/ic_baseline_do_not_touch_24"
|
||||
app:tint="@color/white"
|
||||
tools:visibility="visible" />
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/reaction_animation_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="50dp"
|
||||
android:layout_marginBottom="50dp">
|
||||
</RelativeLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/lower_hand_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
android:layout_marginBottom="@dimen/standard_half_margin"
|
||||
android:contentDescription="@string/lower_hand"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/call_buttons_background"
|
||||
app:borderWidth="0dp"
|
||||
app:fabCustomSize="40dp"
|
||||
app:shapeAppearance="@style/fab_3_rounded"
|
||||
app:srcCompat="@drawable/ic_baseline_do_not_touch_24"
|
||||
app:tint="@color/white"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@+id/callControls"
|
||||
|
|
|
@ -27,7 +27,21 @@
|
|||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/standard_half_padding">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/call_emoji_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/standard_margin"
|
||||
android:layout_marginTop="@dimen/standard_half_margin"
|
||||
android:layout_marginEnd="@dimen/standard_margin"
|
||||
android:layout_marginBottom="@dimen/standard_half_margin"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="10">
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/advanced_call_options_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:gravity="start|center_vertical"
|
||||
|
|
Loading…
Reference in a new issue