mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 19:49:33 +01:00
Issue 2790, allows for continuous voice recording on swipe up, and for previewing messages
- I did a bunch of UI logic in ChatActivity and view_message_input, mainly in setting up the recording interface - I created a custom component, MicInputCloud, under the hood it's 3 ovals, with a hole cut in the center for the icon. The ovals are at around 50% opacity, and they each have their own rotations and size changes animated. General rotation speed and colors can be overridden by the activity implementing it. - I also added a floating action button to activity_chat, to show when the voice recording is locked or not. - I can replay or pause preview voice recordings before sending or deleting - Preview voice recording is now smoother and click boxes are bigger and well defined Signed-off-by: Julius Linus <julius.linus@nextcloud.com>
This commit is contained in:
parent
a4e7a278cf
commit
f0ba16a275
@ -29,6 +29,7 @@
|
|||||||
package com.nextcloud.talk.chat
|
package com.nextcloud.talk.chat
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.animation.ObjectAnimator
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
@ -41,6 +42,8 @@ import android.database.Cursor
|
|||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.media.AudioFormat
|
||||||
|
import android.media.AudioRecord
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@ -63,15 +66,21 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.View.OnTouchListener
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import android.view.animation.AccelerateInterpolator
|
||||||
import android.view.animation.AlphaAnimation
|
import android.view.animation.AlphaAnimation
|
||||||
import android.view.animation.Animation
|
import android.view.animation.Animation
|
||||||
import android.view.animation.LinearInterpolator
|
import android.view.animation.LinearInterpolator
|
||||||
import android.widget.AbsListView
|
import android.widget.AbsListView
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
|
import android.widget.RelativeLayout.BELOW
|
||||||
|
import android.widget.RelativeLayout.LayoutParams
|
||||||
|
import android.widget.SeekBar
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
@ -79,6 +88,7 @@ import androidx.appcompat.view.ContextThemeWrapper
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.PermissionChecker
|
import androidx.core.content.PermissionChecker
|
||||||
|
import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.core.text.bold
|
import androidx.core.text.bold
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
@ -100,6 +110,7 @@ import coil.request.ImageRequest
|
|||||||
import coil.target.Target
|
import coil.target.Target
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
import com.google.android.flexbox.FlexboxLayout
|
import com.google.android.flexbox.FlexboxLayout
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
import com.nextcloud.android.common.ui.theme.utils.ColorRole
|
||||||
import com.nextcloud.talk.BuildConfig
|
import com.nextcloud.talk.BuildConfig
|
||||||
@ -166,6 +177,7 @@ import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
|
|||||||
import com.nextcloud.talk.signaling.SignalingMessageReceiver
|
import com.nextcloud.talk.signaling.SignalingMessageReceiver
|
||||||
import com.nextcloud.talk.signaling.SignalingMessageSender
|
import com.nextcloud.talk.signaling.SignalingMessageSender
|
||||||
import com.nextcloud.talk.translate.ui.TranslateActivity
|
import com.nextcloud.talk.translate.ui.TranslateActivity
|
||||||
|
import com.nextcloud.talk.ui.MicInputCloud
|
||||||
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
|
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
|
||||||
import com.nextcloud.talk.ui.dialog.AttachmentDialog
|
import com.nextcloud.talk.ui.dialog.AttachmentDialog
|
||||||
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
|
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
|
||||||
@ -233,6 +245,8 @@ import javax.inject.Inject
|
|||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private const val l = 50
|
||||||
|
|
||||||
@AutoInjector(NextcloudTalkApplication::class)
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
class ChatActivity :
|
class ChatActivity :
|
||||||
BaseActivity(),
|
BaseActivity(),
|
||||||
@ -312,13 +326,22 @@ class ChatActivity :
|
|||||||
private lateinit var sharedText: String
|
private lateinit var sharedText: String
|
||||||
var isVoiceRecordingInProgress: Boolean = false
|
var isVoiceRecordingInProgress: Boolean = false
|
||||||
var currentVoiceRecordFile: String = ""
|
var currentVoiceRecordFile: String = ""
|
||||||
|
var isVoiceRecordingLocked: Boolean = false
|
||||||
|
private var isVoicePreviewPlaying: Boolean = false
|
||||||
private var recorder: MediaRecorder? = null
|
private var recorder: MediaRecorder? = null
|
||||||
|
private var voicePreviewMediaPlayer: MediaPlayer? = null
|
||||||
|
private var voicePreviewObjectAnimator: ObjectAnimator? = null
|
||||||
var mediaPlayer: MediaPlayer? = null
|
var mediaPlayer: MediaPlayer? = null
|
||||||
lateinit var mediaPlayerHandler: Handler
|
lateinit var mediaPlayerHandler: Handler
|
||||||
private var currentlyPlayedVoiceMessage: ChatMessage? = null
|
private var currentlyPlayedVoiceMessage: ChatMessage? = null
|
||||||
|
private lateinit var micInputAudioRecorder: AudioRecord
|
||||||
|
private var micInputAudioRecordThread: Thread? = null
|
||||||
|
private var isMicInputAudioThreadRunning: Boolean = false
|
||||||
|
private val BUFFER_SIZE = AudioRecord.getMinBufferSize(
|
||||||
|
8000,
|
||||||
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT
|
||||||
|
)
|
||||||
private lateinit var participantPermissions: ParticipantPermissions
|
private lateinit var participantPermissions: ParticipantPermissions
|
||||||
|
|
||||||
private var videoURI: Uri? = null
|
private var videoURI: Uri? = null
|
||||||
@ -461,6 +484,16 @@ class ChatActivity :
|
|||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
active = false
|
active = false
|
||||||
|
stopPreviewVoicePlaying()
|
||||||
|
if (isMicInputAudioThreadRunning) {
|
||||||
|
stopMicInputRecordingAnimation()
|
||||||
|
}
|
||||||
|
if (isVoiceRecordingInProgress) {
|
||||||
|
stopAudioRecording()
|
||||||
|
}
|
||||||
|
if (currentlyPlayedVoiceMessage != null) {
|
||||||
|
stopMediaPlayer(currentlyPlayedVoiceMessage!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@ -593,6 +626,33 @@ class ChatActivity :
|
|||||||
.themeImageButton(it)
|
.themeImageButton(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.findViewById<MaterialButton>(R.id.playPauseBtn)?.let {
|
||||||
|
viewThemeUtils.material.colorMaterialButtonText(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.findViewById<SeekBar>(R.id.seekbar)?.let {
|
||||||
|
viewThemeUtils.platform.themeHorizontalSeekBar(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.findViewById<ImageView>(R.id.deleteVoiceRecording)?.let {
|
||||||
|
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
|
||||||
|
}
|
||||||
|
binding.messageInputView.findViewById<ImageView>(R.id.sendVoiceRecording)?.let {
|
||||||
|
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.findViewById<ImageView>(R.id.microphoneEnabledInfo)?.let {
|
||||||
|
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.findViewById<LinearLayout>(R.id.voice_preview_container)?.let {
|
||||||
|
viewThemeUtils.talk.themeOutgoingMessageBubble(it, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.findViewById<MicInputCloud>(R.id.micInputCloud)?.let {
|
||||||
|
viewThemeUtils.talk.themeMicInputCloud(it)
|
||||||
|
}
|
||||||
|
|
||||||
cancelNotificationsForCurrentConversation()
|
cancelNotificationsForCurrentConversation()
|
||||||
|
|
||||||
chatViewModel.getRoom(conversationUser!!, roomToken)
|
chatViewModel.getRoom(conversationUser!!, roomToken)
|
||||||
@ -626,6 +686,8 @@ class ChatActivity :
|
|||||||
|
|
||||||
binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it.scrollDownButton) }
|
binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it.scrollDownButton) }
|
||||||
|
|
||||||
|
binding.let { viewThemeUtils.material.themeFAB(it.voiceRecordingLock) }
|
||||||
|
|
||||||
binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it.popupBubbleView) }
|
binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it.popupBubbleView) }
|
||||||
|
|
||||||
binding.messageInputView.setPadding(0, 0, 0, 0)
|
binding.messageInputView.setPadding(0, 0, 0, 0)
|
||||||
@ -890,10 +952,18 @@ class ChatActivity :
|
|||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private fun initVoiceRecordButton() {
|
private fun initVoiceRecordButton() {
|
||||||
showMicrophoneButton(true)
|
if (!isVoiceRecordingLocked) {
|
||||||
|
showMicrophoneButton(true)
|
||||||
|
} else if (isVoiceRecordingInProgress) {
|
||||||
|
binding.messageInputView.playPauseBtn.visibility = View.GONE
|
||||||
|
binding.messageInputView.seekBar.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.messageInputView.micInputCloud.setState(MicInputCloud.ViewState.PAUSED_STATE)
|
||||||
|
}
|
||||||
|
|
||||||
binding.messageInputView.messageInput?.doAfterTextChanged {
|
isVoicePreviewPlaying = false
|
||||||
if (binding.messageInputView.messageInput?.text?.isEmpty() == true) {
|
binding.messageInputView.messageInput.doAfterTextChanged {
|
||||||
|
if (binding.messageInputView.messageInput.text?.isEmpty() == true) {
|
||||||
showMicrophoneButton(true)
|
showMicrophoneButton(true)
|
||||||
} else {
|
} else {
|
||||||
showMicrophoneButton(false)
|
showMicrophoneButton(false)
|
||||||
@ -902,12 +972,103 @@ class ChatActivity :
|
|||||||
|
|
||||||
var sliderInitX = 0F
|
var sliderInitX = 0F
|
||||||
var downX = 0f
|
var downX = 0f
|
||||||
|
var originY = 0f
|
||||||
var deltaX = 0f
|
var deltaX = 0f
|
||||||
|
var deltaY = 0f
|
||||||
|
|
||||||
var voiceRecordStartTime = 0L
|
var voiceRecordStartTime = 0L
|
||||||
var voiceRecordEndTime = 0L
|
var voiceRecordEndTime = 0L
|
||||||
|
var voiceRecordPauseTime = 0L
|
||||||
|
val micInputCloudLayoutParams: LayoutParams = binding.messageInputView.micInputCloud
|
||||||
|
.layoutParams as LayoutParams
|
||||||
|
|
||||||
binding.messageInputView.recordAudioButton?.setOnTouchListener(object : View.OnTouchListener {
|
val deleteVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.deleteVoiceRecording
|
||||||
|
.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
val sendVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.sendVoiceRecording
|
||||||
|
.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
// this is so that the seekbar is no longer draggable
|
||||||
|
binding.messageInputView.seekBar.setOnTouchListener(OnTouchListener { _, _ -> true })
|
||||||
|
|
||||||
|
binding.messageInputView.micInputCloud.setOnClickListener {
|
||||||
|
if (isVoiceRecordingInProgress) {
|
||||||
|
recorder?.stop()
|
||||||
|
stopMicInputRecordingAnimation()
|
||||||
|
voiceRecordPauseTime = binding.messageInputView.audioRecordDuration.base - SystemClock.elapsedRealtime()
|
||||||
|
binding.messageInputView.audioRecordDuration.stop()
|
||||||
|
binding.messageInputView.audioRecordDuration.visibility = View.GONE
|
||||||
|
binding.messageInputView.playPauseBtn.visibility = View.VISIBLE
|
||||||
|
binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||||
|
context,
|
||||||
|
R.drawable.ic_baseline_play_arrow_voice_message_24
|
||||||
|
)
|
||||||
|
binding.messageInputView.seekBar.visibility = View.VISIBLE
|
||||||
|
binding.messageInputView.seekBar.progress = 0
|
||||||
|
binding.messageInputView.seekBar.max = 0
|
||||||
|
micInputCloudLayoutParams.removeRule(BELOW)
|
||||||
|
micInputCloudLayoutParams.addRule(BELOW, R.id.voice_preview_container)
|
||||||
|
deleteVoiceRecordingLayoutParams.removeRule(BELOW)
|
||||||
|
deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.voice_preview_container)
|
||||||
|
sendVoiceRecordingLayoutParams.removeRule(BELOW)
|
||||||
|
sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.voice_preview_container)
|
||||||
|
} else {
|
||||||
|
restartAudio()
|
||||||
|
startMicInputRecordingAnimation()
|
||||||
|
binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
|
||||||
|
binding.messageInputView.audioRecordDuration.start()
|
||||||
|
binding.messageInputView.playPauseBtn.visibility = View.GONE
|
||||||
|
binding.messageInputView.seekBar.visibility = View.GONE
|
||||||
|
binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE
|
||||||
|
micInputCloudLayoutParams.removeRule(BELOW)
|
||||||
|
micInputCloudLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
|
||||||
|
deleteVoiceRecordingLayoutParams.removeRule(BELOW)
|
||||||
|
deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
|
||||||
|
sendVoiceRecordingLayoutParams.removeRule(BELOW)
|
||||||
|
sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
isVoiceRecordingInProgress = !isVoiceRecordingInProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.deleteVoiceRecording.setOnClickListener {
|
||||||
|
stopAndDiscardAudioRecording()
|
||||||
|
endVoiceRecordingUI()
|
||||||
|
stopMicInputRecordingAnimation()
|
||||||
|
binding.messageInputView.slideToCancelDescription.x = sliderInitX
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.sendVoiceRecording.setOnClickListener {
|
||||||
|
stopAndSendAudioRecording()
|
||||||
|
endVoiceRecordingUI()
|
||||||
|
stopMicInputRecordingAnimation()
|
||||||
|
binding.messageInputView.slideToCancelDescription.x = sliderInitX
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.playPauseBtn.setOnClickListener {
|
||||||
|
Log.d(TAG, "is voice preview playing $isVoicePreviewPlaying")
|
||||||
|
if (isVoicePreviewPlaying) {
|
||||||
|
Log.d(TAG, "Paused")
|
||||||
|
pausePreviewVoicePlaying()
|
||||||
|
binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||||
|
context,
|
||||||
|
R.drawable
|
||||||
|
.ic_baseline_play_arrow_voice_message_24
|
||||||
|
)
|
||||||
|
isVoicePreviewPlaying = false
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Started")
|
||||||
|
startPreviewVoicePlaying()
|
||||||
|
binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable(
|
||||||
|
context,
|
||||||
|
R.drawable
|
||||||
|
.ic_baseline_pause_voice_message_24
|
||||||
|
)
|
||||||
|
isVoicePreviewPlaying = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.messageInputView.recordAudioButton.setOnTouchListener(object : OnTouchListener {
|
||||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||||
v?.performClick() // ?????????
|
v?.performClick() // ?????????
|
||||||
when (event?.action) {
|
when (event?.action) {
|
||||||
@ -926,6 +1087,7 @@ class ChatActivity :
|
|||||||
setVoiceRecordFileName()
|
setVoiceRecordFileName()
|
||||||
startAudioRecording(currentVoiceRecordFile)
|
startAudioRecording(currentVoiceRecordFile)
|
||||||
downX = event.x
|
downX = event.x
|
||||||
|
originY = event.y
|
||||||
showRecordAudioUi(true)
|
showRecordAudioUi(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -936,13 +1098,16 @@ class ChatActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
stopAndDiscardAudioRecording()
|
stopAndDiscardAudioRecording()
|
||||||
showRecordAudioUi(false)
|
endVoiceRecordingUI()
|
||||||
binding.messageInputView.slideToCancelDescription?.x = sliderInitX
|
binding.messageInputView.slideToCancelDescription.x = sliderInitX
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
Log.d(TAG, "ACTION_UP. stop recording??")
|
Log.d(TAG, "ACTION_UP. stop recording??")
|
||||||
if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
|
if (!isVoiceRecordingInProgress ||
|
||||||
|
!isRecordAudioPermissionGranted() ||
|
||||||
|
isVoiceRecordingLocked
|
||||||
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
showRecordAudioUi(false)
|
showRecordAudioUi(false)
|
||||||
@ -964,7 +1129,7 @@ class ChatActivity :
|
|||||||
stopAndSendAudioRecording()
|
stopAndSendAudioRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.messageInputView.slideToCancelDescription?.x = sliderInitX
|
binding.messageInputView.slideToCancelDescription.x = sliderInitX
|
||||||
}
|
}
|
||||||
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
@ -977,28 +1142,41 @@ class ChatActivity :
|
|||||||
showRecordAudioUi(true)
|
showRecordAudioUi(true)
|
||||||
|
|
||||||
val movedX: Float = event.x
|
val movedX: Float = event.x
|
||||||
|
val movedY: Float = event.y
|
||||||
deltaX = movedX - downX
|
deltaX = movedX - downX
|
||||||
|
deltaY = movedY - originY
|
||||||
|
|
||||||
|
binding.voiceRecordingLock.translationY.let {
|
||||||
|
if (it < VOICE_RECORD_LOCK_BUTTON_Y) {
|
||||||
|
Log.d(TAG, "Voice Recording Locked")
|
||||||
|
isVoiceRecordingLocked = true
|
||||||
|
showVoiceRecordingLocked(true)
|
||||||
|
showVoiceRecordingLockedInterface(true)
|
||||||
|
} else if (deltaY < 0f) {
|
||||||
|
binding.voiceRecordingLock.translationY = deltaY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// only allow slide to left
|
// only allow slide to left
|
||||||
binding.messageInputView.slideToCancelDescription?.x?.let {
|
binding.messageInputView.slideToCancelDescription.x.let {
|
||||||
if (sliderInitX == 0.0F) {
|
if (sliderInitX == 0.0F) {
|
||||||
sliderInitX = it
|
sliderInitX = it
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it > sliderInitX) {
|
if (it > sliderInitX) {
|
||||||
binding.messageInputView.slideToCancelDescription?.x = sliderInitX
|
binding.messageInputView.slideToCancelDescription.x = sliderInitX
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.messageInputView.slideToCancelDescription?.x?.let {
|
binding.messageInputView.slideToCancelDescription.x.let {
|
||||||
if (it < VOICE_RECORD_CANCEL_SLIDER_X) {
|
if (it < VOICE_RECORD_CANCEL_SLIDER_X) {
|
||||||
Log.d(TAG, "stopping recording because slider was moved to left")
|
Log.d(TAG, "stopping recording because slider was moved to left")
|
||||||
stopAndDiscardAudioRecording()
|
stopAndDiscardAudioRecording()
|
||||||
showRecordAudioUi(false)
|
endVoiceRecordingUI()
|
||||||
binding.messageInputView.slideToCancelDescription?.x = sliderInitX
|
binding.messageInputView.slideToCancelDescription.x = sliderInitX
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
binding.messageInputView.slideToCancelDescription?.x = it + deltaX
|
binding.messageInputView.slideToCancelDescription.x = it + deltaX
|
||||||
downX = movedX
|
downX = movedX
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1010,6 +1188,161 @@ class ChatActivity :
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initPreviewVoiceRecording() {
|
||||||
|
voicePreviewMediaPlayer = MediaPlayer().apply {
|
||||||
|
Log.e(TAG, currentVoiceRecordFile)
|
||||||
|
setDataSource(currentVoiceRecordFile)
|
||||||
|
prepare()
|
||||||
|
setOnPreparedListener {
|
||||||
|
Log.d(TAG, "Julius the duration is ${it.duration}")
|
||||||
|
binding.messageInputView.seekBar.progress = 0
|
||||||
|
binding.messageInputView.seekBar.max = it.duration
|
||||||
|
voicePreviewObjectAnimator = ObjectAnimator.ofInt(
|
||||||
|
binding.messageInputView.seekBar,
|
||||||
|
"progress",
|
||||||
|
0,
|
||||||
|
it.duration
|
||||||
|
).apply {
|
||||||
|
duration = it.duration.toLong()
|
||||||
|
interpolator = LinearInterpolator()
|
||||||
|
}
|
||||||
|
voicePreviewMediaPlayer!!.start()
|
||||||
|
voicePreviewObjectAnimator!!.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
setOnCompletionListener {
|
||||||
|
stopPreviewVoicePlaying()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startPreviewVoicePlaying() {
|
||||||
|
Log.d(TAG, "started preview voice recording")
|
||||||
|
if (voicePreviewMediaPlayer == null) {
|
||||||
|
initPreviewVoiceRecording()
|
||||||
|
} else {
|
||||||
|
voicePreviewMediaPlayer!!.start()
|
||||||
|
voicePreviewObjectAnimator!!.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pausePreviewVoicePlaying() {
|
||||||
|
Log.d(TAG, "paused preview voice recording")
|
||||||
|
voicePreviewMediaPlayer!!.pause()
|
||||||
|
voicePreviewObjectAnimator!!.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopPreviewVoicePlaying() {
|
||||||
|
if (voicePreviewMediaPlayer != null) {
|
||||||
|
isVoicePreviewPlaying = false
|
||||||
|
binding.messageInputView.playPauseBtn.icon = ContextCompat.getDrawable(context, R.drawable.ic_refresh)
|
||||||
|
voicePreviewObjectAnimator!!.end()
|
||||||
|
voicePreviewObjectAnimator = null
|
||||||
|
binding.messageInputView.seekBar.clearAnimation()
|
||||||
|
voicePreviewMediaPlayer!!.stop()
|
||||||
|
voicePreviewMediaPlayer!!.release()
|
||||||
|
voicePreviewMediaPlayer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restartAudio() {
|
||||||
|
recorder = MediaRecorder().apply {
|
||||||
|
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||||
|
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
|
||||||
|
setOutputFile(currentVoiceRecordFile)
|
||||||
|
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
|
||||||
|
setAudioSamplingRate(VOICE_MESSAGE_SAMPLING_RATE)
|
||||||
|
setAudioEncodingBitRate(VOICE_MESSAGE_ENCODING_BIT_RATE)
|
||||||
|
setAudioChannels(VOICE_MESSAGE_CHANNELS)
|
||||||
|
prepare()
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun endVoiceRecordingUI() {
|
||||||
|
stopPreviewVoicePlaying()
|
||||||
|
showRecordAudioUi(false)
|
||||||
|
binding.voiceRecordingLock.translationY = 0f
|
||||||
|
isVoiceRecordingLocked = false
|
||||||
|
showVoiceRecordingLocked(false)
|
||||||
|
showVoiceRecordingLockedInterface(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showVoiceRecordingLocked(value: Boolean) {
|
||||||
|
if (value) {
|
||||||
|
binding.voiceRecordingLock.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_lock_grey600_24px)
|
||||||
|
)
|
||||||
|
|
||||||
|
binding.voiceRecordingLock.alpha = 1f
|
||||||
|
binding.voiceRecordingLock.animate().alpha(0f).setDuration(500)
|
||||||
|
.setInterpolator(AccelerateInterpolator()).start()
|
||||||
|
} else {
|
||||||
|
binding.voiceRecordingLock.setImageDrawable(
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_lock_open_grey600_24dp)
|
||||||
|
)
|
||||||
|
binding.voiceRecordingLock.alpha = 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showVoiceRecordingLockedInterface(value: Boolean) {
|
||||||
|
val audioDurationLayoutParams: LayoutParams = binding.messageInputView.audioRecordDuration
|
||||||
|
.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
val micInputCloudLayoutParams: LayoutParams = binding.messageInputView.micInputCloud
|
||||||
|
.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
val deleteVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.deleteVoiceRecording
|
||||||
|
.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
val sendVoiceRecordingLayoutParams: LayoutParams = binding.messageInputView.sendVoiceRecording
|
||||||
|
.layoutParams as LayoutParams
|
||||||
|
|
||||||
|
val standardQuarterMargin = TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
resources.getDimension(R.dimen.standard_quarter_margin),
|
||||||
|
resources
|
||||||
|
.displayMetrics
|
||||||
|
).toInt()
|
||||||
|
|
||||||
|
binding.messageInputView.button.isEnabled = true
|
||||||
|
if (value) {
|
||||||
|
binding.messageInputView.slideToCancelDescription.visibility = View.GONE
|
||||||
|
binding.messageInputView.deleteVoiceRecording.visibility = View.VISIBLE
|
||||||
|
binding.messageInputView.sendVoiceRecording.visibility = View.VISIBLE
|
||||||
|
binding.messageInputView.micInputCloud.visibility = View.VISIBLE
|
||||||
|
binding.messageInputView.recordAudioButton.visibility = View.GONE
|
||||||
|
binding.messageInputView.microphoneEnabledInfo.clearAnimation()
|
||||||
|
binding.messageInputView.microphoneEnabledInfo.visibility = View.GONE
|
||||||
|
binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.GONE
|
||||||
|
binding.messageInputView.recordAudioButton.visibility = View.GONE
|
||||||
|
micInputCloudLayoutParams.removeRule(BELOW)
|
||||||
|
micInputCloudLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
|
||||||
|
deleteVoiceRecordingLayoutParams.removeRule(BELOW)
|
||||||
|
deleteVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
|
||||||
|
sendVoiceRecordingLayoutParams.removeRule(BELOW)
|
||||||
|
sendVoiceRecordingLayoutParams.addRule(BELOW, R.id.audioRecordDuration)
|
||||||
|
audioDurationLayoutParams.removeRule(RelativeLayout.CENTER_VERTICAL)
|
||||||
|
audioDurationLayoutParams.removeRule(RelativeLayout.END_OF)
|
||||||
|
audioDurationLayoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, R.bool.value_true)
|
||||||
|
audioDurationLayoutParams.setMargins(0, standardQuarterMargin, 0, 0)
|
||||||
|
startMicInputRecordingAnimation()
|
||||||
|
Log.d(TAG, "MicInputRecording Started")
|
||||||
|
} else {
|
||||||
|
stopMicInputRecordingAnimation()
|
||||||
|
binding.messageInputView.deleteVoiceRecording.visibility = View.GONE
|
||||||
|
binding.messageInputView.micInputCloud.visibility = View.GONE
|
||||||
|
binding.messageInputView.recordAudioButton.visibility = View.VISIBLE
|
||||||
|
binding.messageInputView.sendVoiceRecording.visibility = View.GONE
|
||||||
|
binding.messageInputView.playPauseBtn.visibility = View.GONE
|
||||||
|
binding.messageInputView.seekBar.visibility = View.GONE
|
||||||
|
audioDurationLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL, R.bool.value_true)
|
||||||
|
audioDurationLayoutParams.addRule(RelativeLayout.END_OF, R.id.microphoneEnabledInfo)
|
||||||
|
audioDurationLayoutParams.removeRule(RelativeLayout.CENTER_HORIZONTAL)
|
||||||
|
audioDurationLayoutParams.setMargins(0, 0, 0, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun initSmileyKeyboardToggler() {
|
private fun initSmileyKeyboardToggler() {
|
||||||
val smileyButton = binding.messageInputView.findViewById<ImageButton>(R.id.smileyButton)
|
val smileyButton = binding.messageInputView.findViewById<ImageButton>(R.id.smileyButton)
|
||||||
|
|
||||||
@ -1452,6 +1785,7 @@ class ChatActivity :
|
|||||||
try {
|
try {
|
||||||
mediaPlayer?.let {
|
mediaPlayer?.let {
|
||||||
if (it.isPlaying) {
|
if (it.isPlaying) {
|
||||||
|
Log.d(TAG, "media player is stopped")
|
||||||
it.stop()
|
it.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1546,24 +1880,83 @@ class ChatActivity :
|
|||||||
|
|
||||||
private fun showRecordAudioUi(show: Boolean) {
|
private fun showRecordAudioUi(show: Boolean) {
|
||||||
if (show) {
|
if (show) {
|
||||||
binding.messageInputView.microphoneEnabledInfo?.visibility = View.VISIBLE
|
binding.messageInputView.microphoneEnabledInfo.visibility = View.VISIBLE
|
||||||
binding.messageInputView.microphoneEnabledInfoBackground?.visibility = View.VISIBLE
|
binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.VISIBLE
|
||||||
binding.messageInputView.audioRecordDuration?.visibility = View.VISIBLE
|
binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE
|
||||||
binding.messageInputView.slideToCancelDescription?.visibility = View.VISIBLE
|
binding.messageInputView.slideToCancelDescription.visibility = View.VISIBLE
|
||||||
binding.messageInputView.attachmentButton?.visibility = View.GONE
|
binding.messageInputView.attachmentButton.visibility = View.GONE
|
||||||
binding.messageInputView.smileyButton?.visibility = View.GONE
|
binding.messageInputView.smileyButton.visibility = View.GONE
|
||||||
binding.messageInputView.messageInput?.visibility = View.GONE
|
binding.messageInputView.messageInput.visibility = View.GONE
|
||||||
binding.messageInputView.messageInput?.hint = ""
|
binding.messageInputView.messageInput.hint = ""
|
||||||
|
binding.voiceRecordingLock.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
binding.messageInputView.microphoneEnabledInfo?.visibility = View.GONE
|
binding.messageInputView.microphoneEnabledInfo.visibility = View.GONE
|
||||||
binding.messageInputView.microphoneEnabledInfoBackground?.visibility = View.GONE
|
binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.GONE
|
||||||
binding.messageInputView.audioRecordDuration?.visibility = View.GONE
|
binding.messageInputView.audioRecordDuration.visibility = View.GONE
|
||||||
binding.messageInputView.slideToCancelDescription?.visibility = View.GONE
|
binding.messageInputView.slideToCancelDescription.visibility = View.GONE
|
||||||
binding.messageInputView.attachmentButton?.visibility = View.VISIBLE
|
binding.messageInputView.attachmentButton.visibility = View.VISIBLE
|
||||||
binding.messageInputView.smileyButton?.visibility = View.VISIBLE
|
binding.messageInputView.smileyButton.visibility = View.VISIBLE
|
||||||
binding.messageInputView.messageInput?.visibility = View.VISIBLE
|
binding.messageInputView.messageInput.visibility = View.VISIBLE
|
||||||
binding.messageInputView.messageInput?.hint =
|
binding.messageInputView.messageInput.hint =
|
||||||
context.resources?.getString(R.string.nc_hint_enter_a_message)
|
context.resources?.getString(R.string.nc_hint_enter_a_message)
|
||||||
|
binding.voiceRecordingLock.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMicInputRecordingAnimation() {
|
||||||
|
val permissionCheck = ContextCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
)
|
||||||
|
|
||||||
|
if (micInputAudioRecordThread == null && permissionCheck == PERMISSION_GRANTED) {
|
||||||
|
Log.d(TAG, "Mic Animation Started")
|
||||||
|
micInputAudioRecorder = AudioRecord(
|
||||||
|
MediaRecorder.AudioSource.MIC,
|
||||||
|
8000,
|
||||||
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
BUFFER_SIZE
|
||||||
|
)
|
||||||
|
isMicInputAudioThreadRunning = true
|
||||||
|
micInputAudioRecorder.startRecording()
|
||||||
|
micInputAudioRecordThread = Thread(
|
||||||
|
Runnable {
|
||||||
|
while (isMicInputAudioThreadRunning) {
|
||||||
|
val byteArr = ByteArray(BUFFER_SIZE / 2)
|
||||||
|
micInputAudioRecorder.read(byteArr, 0, byteArr.size)
|
||||||
|
val d = Math.abs(byteArr[0].toDouble())
|
||||||
|
if (d > 40) {
|
||||||
|
binding.messageInputView.micInputCloud.setRotationSpeed(
|
||||||
|
Math.log10(d).toFloat(),
|
||||||
|
MicInputCloud.MAXIMUM_RADIUS
|
||||||
|
)
|
||||||
|
} else if (d > 20) {
|
||||||
|
binding.messageInputView.micInputCloud.setRotationSpeed(
|
||||||
|
Math.log10(d).toFloat(),
|
||||||
|
MicInputCloud.EXTENDED_RADIUS
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
binding.messageInputView.micInputCloud.setRotationSpeed(
|
||||||
|
1f,
|
||||||
|
MicInputCloud.DEFAULT_RADIUS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Thread.sleep(50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
micInputAudioRecordThread!!.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopMicInputRecordingAnimation() {
|
||||||
|
if (micInputAudioRecordThread != null) {
|
||||||
|
Log.d(TAG, "Mic Animation Ended")
|
||||||
|
micInputAudioRecorder.stop()
|
||||||
|
micInputAudioRecorder.release()
|
||||||
|
isMicInputAudioThreadRunning = false
|
||||||
|
micInputAudioRecordThread = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1571,19 +1964,19 @@ class ChatActivity :
|
|||||||
return PermissionChecker.checkSelfPermission(
|
return PermissionChecker.checkSelfPermission(
|
||||||
context,
|
context,
|
||||||
Manifest.permission.RECORD_AUDIO
|
Manifest.permission.RECORD_AUDIO
|
||||||
) == PermissionChecker.PERMISSION_GRANTED
|
) == PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startAudioRecording(file: String) {
|
private fun startAudioRecording(file: String) {
|
||||||
binding.messageInputView.audioRecordDuration?.base = SystemClock.elapsedRealtime()
|
binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
|
||||||
binding.messageInputView.audioRecordDuration?.start()
|
binding.messageInputView.audioRecordDuration.start()
|
||||||
|
|
||||||
val animation: Animation = AlphaAnimation(1.0f, 0.0f)
|
val animation: Animation = AlphaAnimation(1.0f, 0.0f)
|
||||||
animation.duration = ANIMATION_DURATION
|
animation.duration = ANIMATION_DURATION
|
||||||
animation.interpolator = LinearInterpolator()
|
animation.interpolator = LinearInterpolator()
|
||||||
animation.repeatCount = Animation.INFINITE
|
animation.repeatCount = Animation.INFINITE
|
||||||
animation.repeatMode = Animation.REVERSE
|
animation.repeatMode = Animation.REVERSE
|
||||||
binding.messageInputView.microphoneEnabledInfo?.startAnimation(animation)
|
binding.messageInputView.microphoneEnabledInfo.startAnimation(animation)
|
||||||
|
|
||||||
recorder = MediaRecorder().apply {
|
recorder = MediaRecorder().apply {
|
||||||
setAudioSource(MediaRecorder.AudioSource.MIC)
|
setAudioSource(MediaRecorder.AudioSource.MIC)
|
||||||
@ -1612,13 +2005,18 @@ class ChatActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun stopAndSendAudioRecording() {
|
private fun stopAndSendAudioRecording() {
|
||||||
stopAudioRecording()
|
if (isVoiceRecordingInProgress) {
|
||||||
|
stopAudioRecording()
|
||||||
|
}
|
||||||
|
|
||||||
val uri = Uri.fromFile(File(currentVoiceRecordFile))
|
val uri = Uri.fromFile(File(currentVoiceRecordFile))
|
||||||
uploadFile(uri.toString(), true)
|
uploadFile(uri.toString(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopAndDiscardAudioRecording() {
|
private fun stopAndDiscardAudioRecording() {
|
||||||
stopAudioRecording()
|
if (isVoiceRecordingInProgress) {
|
||||||
|
stopAudioRecording()
|
||||||
|
}
|
||||||
|
|
||||||
val cachedFile = File(currentVoiceRecordFile)
|
val cachedFile = File(currentVoiceRecordFile)
|
||||||
cachedFile.delete()
|
cachedFile.delete()
|
||||||
@ -1626,26 +2024,22 @@ class ChatActivity :
|
|||||||
|
|
||||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||||
private fun stopAudioRecording() {
|
private fun stopAudioRecording() {
|
||||||
binding.messageInputView.audioRecordDuration?.stop()
|
binding.messageInputView.audioRecordDuration.stop()
|
||||||
binding.messageInputView.microphoneEnabledInfo?.clearAnimation()
|
binding.messageInputView.microphoneEnabledInfo.clearAnimation()
|
||||||
|
|
||||||
if (isVoiceRecordingInProgress) {
|
recorder?.apply {
|
||||||
recorder?.apply {
|
try {
|
||||||
try {
|
stop()
|
||||||
stop()
|
release()
|
||||||
release()
|
isVoiceRecordingInProgress = false
|
||||||
isVoiceRecordingInProgress = false
|
Log.d(TAG, "stopped recorder. isVoiceRecordingInProgress = false")
|
||||||
Log.d(TAG, "stopped recorder. isVoiceRecordingInProgress = false")
|
} catch (e: RuntimeException) {
|
||||||
} catch (e: RuntimeException) {
|
Log.w(TAG, "error while stopping recorder!")
|
||||||
Log.w(TAG, "error while stopping recorder!")
|
|
||||||
}
|
|
||||||
|
|
||||||
VibrationUtils.vibrateShort(context)
|
|
||||||
}
|
}
|
||||||
recorder = null
|
|
||||||
} else {
|
VibrationUtils.vibrateShort(context)
|
||||||
Log.e(TAG, "tried to stop audio recorder but it was not recording")
|
|
||||||
}
|
}
|
||||||
|
recorder = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestRecordAudioPermissions() {
|
private fun requestRecordAudioPermissions() {
|
||||||
@ -1761,7 +2155,7 @@ class ChatActivity :
|
|||||||
ConversationUtils.isLobbyViewApplicable(currentConversation!!, conversationUser!!)
|
ConversationUtils.isLobbyViewApplicable(currentConversation!!, conversationUser!!)
|
||||||
) {
|
) {
|
||||||
if (shouldShowLobby()) {
|
if (shouldShowLobby()) {
|
||||||
binding.lobby.lobbyView?.visibility = View.VISIBLE
|
binding.lobby.lobbyView.visibility = View.VISIBLE
|
||||||
binding.messagesListView.visibility = View.GONE
|
binding.messagesListView.visibility = View.GONE
|
||||||
binding.messageInputView.visibility = View.GONE
|
binding.messageInputView.visibility = View.GONE
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
@ -1785,9 +2179,9 @@ class ChatActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
sb.append(currentConversation!!.description)
|
sb.append(currentConversation!!.description)
|
||||||
binding.lobby.lobbyTextView?.text = sb.toString()
|
binding.lobby.lobbyTextView.text = sb.toString()
|
||||||
} else {
|
} else {
|
||||||
binding.lobby.lobbyView?.visibility = View.GONE
|
binding.lobby.lobbyView.visibility = View.GONE
|
||||||
binding.messagesListView.visibility = View.VISIBLE
|
binding.messagesListView.visibility = View.VISIBLE
|
||||||
binding.messageInputView.inputEditText?.visibility = View.VISIBLE
|
binding.messageInputView.inputEditText?.visibility = View.VISIBLE
|
||||||
if (isFirstMessagesProcessing && pastPreconditionFailed) {
|
if (isFirstMessagesProcessing && pastPreconditionFailed) {
|
||||||
@ -1799,7 +2193,7 @@ class ChatActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.lobby.lobbyView?.visibility = View.GONE
|
binding.lobby.lobbyView.visibility = View.GONE
|
||||||
binding.messagesListView.visibility = View.VISIBLE
|
binding.messagesListView.visibility = View.VISIBLE
|
||||||
binding.messageInputView.inputEditText?.visibility = View.VISIBLE
|
binding.messageInputView.inputEditText?.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
@ -2774,7 +3168,7 @@ class ChatActivity :
|
|||||||
|
|
||||||
private fun modifyMessageCount(shouldAddNewMessagesNotice: Boolean, shouldScroll: Boolean) {
|
private fun modifyMessageCount(shouldAddNewMessagesNotice: Boolean, shouldScroll: Boolean) {
|
||||||
if (!shouldAddNewMessagesNotice && !shouldScroll) {
|
if (!shouldAddNewMessagesNotice && !shouldScroll) {
|
||||||
binding.popupBubbleView.isShown?.let {
|
binding.popupBubbleView.isShown.let {
|
||||||
if (it) {
|
if (it) {
|
||||||
newMessagesCount++
|
newMessagesCount++
|
||||||
} else {
|
} else {
|
||||||
@ -3455,11 +3849,11 @@ class ChatActivity :
|
|||||||
|
|
||||||
private fun showMicrophoneButton(show: Boolean) {
|
private fun showMicrophoneButton(show: Boolean) {
|
||||||
if (show && CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "voice-message-sharing")) {
|
if (show && CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "voice-message-sharing")) {
|
||||||
binding.messageInputView.messageSendButton?.visibility = View.GONE
|
binding.messageInputView.messageSendButton.visibility = View.GONE
|
||||||
binding.messageInputView.recordAudioButton?.visibility = View.VISIBLE
|
binding.messageInputView.recordAudioButton.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
binding.messageInputView.messageSendButton?.visibility = View.VISIBLE
|
binding.messageInputView.messageSendButton.visibility = View.VISIBLE
|
||||||
binding.messageInputView.recordAudioButton?.visibility = View.GONE
|
binding.messageInputView.recordAudioButton.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3727,6 +4121,7 @@ class ChatActivity :
|
|||||||
private const val OBJECT_MESSAGE: String = "{object}"
|
private const val OBJECT_MESSAGE: String = "{object}"
|
||||||
private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
|
private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
|
||||||
private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50
|
private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50
|
||||||
|
private const val VOICE_RECORD_LOCK_BUTTON_Y: Int = -130
|
||||||
private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
|
private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
|
||||||
private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
|
private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
|
||||||
|
|
||||||
|
@ -25,8 +25,10 @@ import android.util.AttributeSet
|
|||||||
import android.widget.Chronometer
|
import android.widget.Chronometer
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
import android.widget.SeekBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.emoji2.widget.EmojiEditText
|
import androidx.emoji2.widget.EmojiEditText
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.nextcloud.talk.R
|
import com.nextcloud.talk.R
|
||||||
import com.stfalcon.chatkit.messages.MessageInput
|
import com.stfalcon.chatkit.messages.MessageInput
|
||||||
|
|
||||||
@ -37,6 +39,11 @@ class MessageInput : MessageInput {
|
|||||||
lateinit var microphoneEnabledInfo: ImageView
|
lateinit var microphoneEnabledInfo: ImageView
|
||||||
lateinit var microphoneEnabledInfoBackground: ImageView
|
lateinit var microphoneEnabledInfoBackground: ImageView
|
||||||
lateinit var smileyButton: ImageButton
|
lateinit var smileyButton: ImageButton
|
||||||
|
lateinit var deleteVoiceRecording: ImageView
|
||||||
|
lateinit var sendVoiceRecording: ImageView
|
||||||
|
lateinit var micInputCloud: MicInputCloud
|
||||||
|
lateinit var playPauseBtn: MaterialButton
|
||||||
|
lateinit var seekBar: SeekBar
|
||||||
|
|
||||||
constructor(context: Context?) : super(context) {
|
constructor(context: Context?) : super(context) {
|
||||||
init()
|
init()
|
||||||
@ -57,6 +64,11 @@ class MessageInput : MessageInput {
|
|||||||
microphoneEnabledInfo = findViewById(R.id.microphoneEnabledInfo)
|
microphoneEnabledInfo = findViewById(R.id.microphoneEnabledInfo)
|
||||||
microphoneEnabledInfoBackground = findViewById(R.id.microphoneEnabledInfoBackground)
|
microphoneEnabledInfoBackground = findViewById(R.id.microphoneEnabledInfoBackground)
|
||||||
smileyButton = findViewById(R.id.smileyButton)
|
smileyButton = findViewById(R.id.smileyButton)
|
||||||
|
deleteVoiceRecording = findViewById(R.id.deleteVoiceRecording)
|
||||||
|
sendVoiceRecording = findViewById(R.id.sendVoiceRecording)
|
||||||
|
micInputCloud = findViewById(R.id.micInputCloud)
|
||||||
|
playPauseBtn = findViewById(R.id.playPauseBtn)
|
||||||
|
seekBar = findViewById(R.id.seekbar)
|
||||||
}
|
}
|
||||||
|
|
||||||
var messageInput: EmojiEditText
|
var messageInput: EmojiEditText
|
||||||
|
369
app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt
Normal file
369
app/src/main/java/com/nextcloud/talk/ui/MicInputCloud.kt
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Julius Linus
|
||||||
|
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
|
||||||
|
*
|
||||||
|
* 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.ui
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Paint.ANTI_ALIAS_FLAG
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.VectorDrawable
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.animation.LinearInterpolator
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import com.nextcloud.talk.R
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
class MicInputCloud(context: Context, attrs: AttributeSet) : View(context, attrs) {
|
||||||
|
/**
|
||||||
|
* State Descriptions:
|
||||||
|
* - PAUSED_STATE: Animation speed is set to zero
|
||||||
|
* - PLAY_STATE: Animation speed is set to default, but can be overridden
|
||||||
|
*/
|
||||||
|
enum class ViewState {
|
||||||
|
PAUSED_STATE,
|
||||||
|
PLAY_STATE
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private var primaryColor: Int = Color.WHITE
|
||||||
|
|
||||||
|
private var pauseIcon: VectorDrawable? = null
|
||||||
|
|
||||||
|
private var playIcon: VectorDrawable? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
context.theme.obtainStyledAttributes(
|
||||||
|
attrs,
|
||||||
|
R.styleable.MicInputCloud,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
).apply {
|
||||||
|
|
||||||
|
try {
|
||||||
|
pauseIcon = getDrawable(R.styleable.MicInputCloud_pauseIcon) as VectorDrawable
|
||||||
|
playIcon = getDrawable(R.styleable.MicInputCloud_playIcon) as VectorDrawable
|
||||||
|
} finally {
|
||||||
|
recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var state: ViewState = ViewState.PLAY_STATE
|
||||||
|
private var ovalOneAnimator: ValueAnimator? = null
|
||||||
|
private var ovalTwoAnimator: ValueAnimator? = null
|
||||||
|
private var ovalThreeAnimator: ValueAnimator? = null
|
||||||
|
private var r1 = OVAL_ONE_DEFAULT_ROTATION
|
||||||
|
private var r2 = OVAL_TWO_DEFAULT_ROTATION
|
||||||
|
private var r3 = OVAL_THREE_DEFAULT_ROTATION
|
||||||
|
private var o1h = OVAL_ONE_DEFAULT_HEIGHT
|
||||||
|
private var o1w = OVAL_ONE_DEFAULT_WIDTH
|
||||||
|
private var o2h = OVAL_TWO_DEFAULT_HEIGHT
|
||||||
|
private var o2w = OVAL_TWO_DEFAULT_WIDTH
|
||||||
|
private var o3h = OVAL_THREE_DEFAULT_HEIGHT
|
||||||
|
private var o3w = OVAL_THREE_DEFAULT_WIDTH
|
||||||
|
private var rotationSpeedMultiplier: Float = DEFAULT_ROTATION_SPEED_MULTIPLIER
|
||||||
|
private var radius: Float = DEFAULT_RADIUS
|
||||||
|
private var centerX: Float = 0f
|
||||||
|
private var centerY: Float = 0f
|
||||||
|
|
||||||
|
private val bottomCirclePaint = Paint(ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = primaryColor
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
alpha = DEFAULT_OPACITY
|
||||||
|
}
|
||||||
|
|
||||||
|
private val topCircleBounds = Rect(0, 0, 0, 0)
|
||||||
|
|
||||||
|
override fun onVisibilityChanged(changedView: View, visibility: Int) {
|
||||||
|
super.onVisibilityChanged(changedView, visibility)
|
||||||
|
if (visibility == VISIBLE) {
|
||||||
|
createAnimators()
|
||||||
|
} else {
|
||||||
|
state = ViewState.PLAY_STATE
|
||||||
|
destroyAnimators()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAnimators() {
|
||||||
|
ovalOneAnimator = ValueAnimator.ofInt(
|
||||||
|
o1h,
|
||||||
|
OVAL_ONE_DEFAULT_HEIGHT + ANIMATION_CAP,
|
||||||
|
o1h
|
||||||
|
).apply {
|
||||||
|
duration = OVAL_ONE_ANIMATION_LENGTH
|
||||||
|
interpolator = LinearInterpolator()
|
||||||
|
repeatCount = ValueAnimator.INFINITE
|
||||||
|
addUpdateListener { valueAnimator ->
|
||||||
|
o1h = valueAnimator.animatedValue as Int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ovalTwoAnimator = ValueAnimator.ofInt(
|
||||||
|
o2h,
|
||||||
|
OVAL_TWO_DEFAULT_HEIGHT + ANIMATION_CAP,
|
||||||
|
o2h
|
||||||
|
).apply {
|
||||||
|
duration = OVAL_TWO_ANIMATION_LENGTH
|
||||||
|
interpolator = LinearInterpolator()
|
||||||
|
repeatCount = ValueAnimator.INFINITE
|
||||||
|
addUpdateListener { valueAnimator ->
|
||||||
|
o2h = valueAnimator.animatedValue as Int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ovalThreeAnimator = ValueAnimator.ofInt(
|
||||||
|
o3h,
|
||||||
|
OVAL_THREE_DEFAULT_HEIGHT + ANIMATION_CAP,
|
||||||
|
o3h
|
||||||
|
).apply {
|
||||||
|
duration = OVAL_THREE_ANIMATION_LENGTH
|
||||||
|
interpolator = LinearInterpolator()
|
||||||
|
repeatCount = ValueAnimator.INFINITE
|
||||||
|
addUpdateListener { valueAnimator ->
|
||||||
|
o3h = valueAnimator.animatedValue as Int
|
||||||
|
invalidate() // needed to animate the other listeners as well
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ovalOneAnimator?.start()
|
||||||
|
ovalTwoAnimator?.start()
|
||||||
|
ovalThreeAnimator?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun destroyAnimators() {
|
||||||
|
ovalOneAnimator?.cancel()
|
||||||
|
ovalOneAnimator?.removeAllUpdateListeners()
|
||||||
|
ovalTwoAnimator?.cancel()
|
||||||
|
ovalTwoAnimator?.removeAllUpdateListeners()
|
||||||
|
ovalThreeAnimator?.cancel()
|
||||||
|
ovalThreeAnimator?.removeAllUpdateListeners()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val circlePath: Path = Path()
|
||||||
|
private val ovalOnePath: Path = Path()
|
||||||
|
private val ovalTwoPath: Path = Path()
|
||||||
|
private val ovalThreePath: Path = Path()
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas?) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
circlePath.apply {
|
||||||
|
addCircle(centerX, centerY, DEFAULT_RADIUS, Path.Direction.CCW)
|
||||||
|
}
|
||||||
|
ovalOnePath.apply {
|
||||||
|
addOval(
|
||||||
|
centerX - (radius + o1w),
|
||||||
|
centerY - o1h,
|
||||||
|
centerX + (radius + o1w),
|
||||||
|
centerY + o1h,
|
||||||
|
Path.Direction.CCW
|
||||||
|
)
|
||||||
|
op(this, circlePath, Path.Op.DIFFERENCE)
|
||||||
|
}
|
||||||
|
ovalTwoPath.apply {
|
||||||
|
addOval(
|
||||||
|
centerX - (radius + o2w),
|
||||||
|
centerY - o2h,
|
||||||
|
centerX + (radius + o2w),
|
||||||
|
centerY + o2h,
|
||||||
|
Path.Direction.CCW
|
||||||
|
)
|
||||||
|
op(this, circlePath, Path.Op.DIFFERENCE)
|
||||||
|
}
|
||||||
|
ovalThreePath.apply {
|
||||||
|
addOval(
|
||||||
|
centerX - (radius + o3w),
|
||||||
|
centerY - o3h,
|
||||||
|
centerX + (radius + o3w),
|
||||||
|
centerY + o3h,
|
||||||
|
Path.Direction.CCW
|
||||||
|
)
|
||||||
|
op(this, circlePath, Path.Op.DIFFERENCE)
|
||||||
|
}
|
||||||
|
drawMicInputCloud(canvas)
|
||||||
|
if (state == ViewState.PLAY_STATE) {
|
||||||
|
r1 += OVAL_ONE_ANIMATION_SPEED * rotationSpeedMultiplier
|
||||||
|
r2 -= OVAL_TWO_ANIMATION_SPEED * rotationSpeedMultiplier
|
||||||
|
r3 += OVAL_THREE_ANIMATION_SPEED * rotationSpeedMultiplier
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawMicInputCloud(canvas: Canvas?) {
|
||||||
|
canvas?.apply {
|
||||||
|
save()
|
||||||
|
rotate(r1, centerX, centerY)
|
||||||
|
drawPath(ovalOnePath, bottomCirclePaint)
|
||||||
|
restore()
|
||||||
|
save()
|
||||||
|
rotate(r2, centerX, centerY)
|
||||||
|
drawPath(ovalTwoPath, bottomCirclePaint)
|
||||||
|
restore()
|
||||||
|
save()
|
||||||
|
rotate(r3, centerX, centerY)
|
||||||
|
drawPath(ovalThreePath, bottomCirclePaint)
|
||||||
|
restore()
|
||||||
|
circlePath.reset()
|
||||||
|
ovalOnePath.reset()
|
||||||
|
ovalTwoPath.reset()
|
||||||
|
ovalThreePath.reset()
|
||||||
|
if (state == ViewState.PLAY_STATE) {
|
||||||
|
pauseIcon?.apply {
|
||||||
|
bounds = topCircleBounds
|
||||||
|
setTint(primaryColor)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
playIcon?.apply {
|
||||||
|
bounds = topCircleBounds
|
||||||
|
setTint(primaryColor)
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||||
|
val desiredWidth = DEFAULT_SIZE.dp
|
||||||
|
val desiredHeight = DEFAULT_SIZE.dp
|
||||||
|
|
||||||
|
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
||||||
|
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
|
||||||
|
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
||||||
|
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
|
||||||
|
|
||||||
|
val width: Int = when (widthMode) {
|
||||||
|
MeasureSpec.EXACTLY -> {
|
||||||
|
widthSize
|
||||||
|
}
|
||||||
|
|
||||||
|
MeasureSpec.AT_MOST -> {
|
||||||
|
desiredWidth.coerceAtMost(widthSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
desiredWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val height: Int = when (heightMode) {
|
||||||
|
MeasureSpec.EXACTLY -> {
|
||||||
|
heightSize
|
||||||
|
}
|
||||||
|
|
||||||
|
MeasureSpec.AT_MOST -> {
|
||||||
|
desiredHeight.coerceAtMost(heightSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
desiredHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
centerX = (width / 2).toFloat()
|
||||||
|
centerY = (height / 2).toFloat()
|
||||||
|
topCircleBounds.left = (centerX - DEFAULT_RADIUS).toInt()
|
||||||
|
topCircleBounds.top = (centerY - DEFAULT_RADIUS).toInt()
|
||||||
|
topCircleBounds.right = (centerX + DEFAULT_RADIUS).toInt()
|
||||||
|
topCircleBounds.bottom = (centerY + DEFAULT_RADIUS).toInt()
|
||||||
|
|
||||||
|
setMeasuredDimension(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun performClick(): Boolean {
|
||||||
|
state = if (state == ViewState.PAUSED_STATE) {
|
||||||
|
ovalOneAnimator?.resume()
|
||||||
|
ovalTwoAnimator?.resume()
|
||||||
|
ovalThreeAnimator?.resume()
|
||||||
|
ViewState.PLAY_STATE
|
||||||
|
} else {
|
||||||
|
ovalOneAnimator?.pause()
|
||||||
|
ovalTwoAnimator?.pause()
|
||||||
|
ovalThreeAnimator?.pause()
|
||||||
|
ViewState.PAUSED_STATE
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
return super.performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the color of the cloud to the parameter, opacity is still set to 50%
|
||||||
|
*/
|
||||||
|
fun setColor(primary: Int) {
|
||||||
|
primaryColor = primary
|
||||||
|
bottomCirclePaint.apply {
|
||||||
|
color = primary
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
alpha = DEFAULT_OPACITY
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets state of the component to the parameter
|
||||||
|
*/
|
||||||
|
fun setState(s: ViewState) {
|
||||||
|
state = s
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the rotation speed and radius to the parameters, defaults are left unchanged
|
||||||
|
*/
|
||||||
|
fun setRotationSpeed(speed: Float, r: Float) {
|
||||||
|
rotationSpeedMultiplier = speed
|
||||||
|
radius = r
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG: String? = MicInputCloud::class.simpleName
|
||||||
|
const val DEFAULT_RADIUS: Float = 70f
|
||||||
|
const val EXTENDED_RADIUS: Float = 75f
|
||||||
|
const val MAXIMUM_RADIUS: Float = 80f
|
||||||
|
private const val DEFAULT_SIZE: Int = 110
|
||||||
|
private const val DEFAULT_OPACITY: Int = 108
|
||||||
|
private const val DEFAULT_ROTATION_SPEED_MULTIPLIER: Float = 0.5f
|
||||||
|
private const val OVAL_ONE_DEFAULT_ROTATION: Float = 105f
|
||||||
|
private const val OVAL_ONE_DEFAULT_HEIGHT: Int = 85
|
||||||
|
private const val OVAL_ONE_DEFAULT_WIDTH: Int = 30
|
||||||
|
private const val OVAL_ONE_ANIMATION_LENGTH: Long = 2000
|
||||||
|
private const val OVAL_ONE_ANIMATION_SPEED: Float = 2.3f
|
||||||
|
private const val OVAL_TWO_DEFAULT_ROTATION: Float = 138f
|
||||||
|
private const val OVAL_TWO_DEFAULT_HEIGHT: Int = 70
|
||||||
|
private const val OVAL_TWO_DEFAULT_WIDTH: Int = 25
|
||||||
|
private const val OVAL_TWO_ANIMATION_LENGTH: Long = 1000
|
||||||
|
private const val OVAL_TWO_ANIMATION_SPEED: Float = 1.75f
|
||||||
|
private const val OVAL_THREE_DEFAULT_ROTATION: Float = 63f
|
||||||
|
private const val OVAL_THREE_DEFAULT_HEIGHT: Int = 80
|
||||||
|
private const val OVAL_THREE_DEFAULT_WIDTH: Int = 40
|
||||||
|
private const val OVAL_THREE_ANIMATION_LENGTH: Long = 1500
|
||||||
|
private const val OVAL_THREE_ANIMATION_SPEED: Float = 1f
|
||||||
|
private const val ANIMATION_CAP: Int = 15
|
||||||
|
private val Int.dp: Int
|
||||||
|
get() = (this * Resources.getSystem().displayMetrics.density).roundToInt()
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,7 @@ import com.nextcloud.android.common.ui.theme.MaterialSchemes
|
|||||||
import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase
|
import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase
|
||||||
import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils
|
import com.nextcloud.android.common.ui.theme.utils.AndroidXViewThemeUtils
|
||||||
import com.nextcloud.talk.R
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.ui.MicInputCloud
|
||||||
import com.nextcloud.talk.utils.DisplayUtils
|
import com.nextcloud.talk.utils.DisplayUtils
|
||||||
import com.nextcloud.talk.utils.DrawableUtils
|
import com.nextcloud.talk.utils.DrawableUtils
|
||||||
import com.vanniktech.emoji.EmojiTextView
|
import com.vanniktech.emoji.EmojiTextView
|
||||||
@ -243,6 +244,12 @@ class TalkSpecificViewThemeUtils @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun themeMicInputCloud(micInputCloud: MicInputCloud) {
|
||||||
|
withScheme(micInputCloud) { scheme ->
|
||||||
|
micInputCloud.setColor(scheme.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val THEMEABLE_PLACEHOLDER_IDS = listOf(
|
private val THEMEABLE_PLACEHOLDER_IDS = listOf(
|
||||||
R.drawable.ic_mimetype_package_x_generic,
|
R.drawable.ic_mimetype_package_x_generic,
|
||||||
|
27
app/src/main/res/drawable/baseline_stop_24.xml
Normal file
27
app/src/main/res/drawable/baseline_stop_24.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!--
|
||||||
|
@author Google LLC
|
||||||
|
Copyright (C) 2023 Google LLC
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector android:height="24dp"
|
||||||
|
android:tint="#000000"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:width="24dp"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,6h12v12H6z" />
|
||||||
|
</vector>
|
@ -132,6 +132,18 @@
|
|||||||
app:cornerRadius="@dimen/button_corner_radius"
|
app:cornerRadius="@dimen/button_corner_radius"
|
||||||
app:icon="@drawable/ic_baseline_arrow_downward_24px" />
|
app:icon="@drawable/ic_baseline_arrow_downward_24px" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/voice_recording_lock"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
style="?attr/floatingActionButtonSmallStyle"
|
||||||
|
android:layout_marginEnd="@dimen/standard_margin"
|
||||||
|
android:layout_marginBottom="@dimen/standard_margin"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
app:srcCompat="@drawable/ic_lock_open_grey600_24dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/scrollDownButton"
|
android:id="@+id/scrollDownButton"
|
||||||
style="@style/Widget.AppTheme.Button.ElevatedButton"
|
style="@style/Widget.AppTheme.Button.ElevatedButton"
|
||||||
|
@ -109,6 +109,47 @@
|
|||||||
tools:visibility="gone"
|
tools:visibility="gone"
|
||||||
android:contentDescription="@null" />
|
android:contentDescription="@null" />
|
||||||
|
|
||||||
|
<com.nextcloud.talk.ui.MicInputCloud
|
||||||
|
android:id="@+id/micInputCloud"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/audioRecordDuration"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
app:playIcon="@drawable/ic_refresh"
|
||||||
|
app:pauseIcon="@drawable/baseline_stop_24"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/deleteVoiceRecording"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignBottom="@id/micInputCloud"
|
||||||
|
android:layout_below="@id/audioRecordDuration"
|
||||||
|
android:layout_marginVertical="@dimen/standard_margin"
|
||||||
|
android:layout_marginStart="@dimen/standard_double_margin"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:src="@drawable/ic_delete"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/sendVoiceRecording"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignBottom="@id/micInputCloud"
|
||||||
|
android:layout_below="@id/audioRecordDuration"
|
||||||
|
android:layout_marginVertical="@dimen/standard_margin"
|
||||||
|
android:layout_marginEnd="@dimen/standard_double_margin"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:src="@drawable/ic_send"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<!-- the height of this ImageView is used to define the overall height of the
|
<!-- the height of this ImageView is used to define the overall height of the
|
||||||
parent layout whenever the voice recording mode is enabled. parent layout has
|
parent layout whenever the voice recording mode is enabled. parent layout has
|
||||||
height=wrap_content because it must enlarge whenever user types a message with
|
height=wrap_content because it must enlarge whenever user types a message with
|
||||||
@ -123,7 +164,43 @@
|
|||||||
android:src="@drawable/ic_baseline_mic_red_24"
|
android:src="@drawable/ic_baseline_mic_red_24"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="gone"/>
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/voice_preview_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/quotedChatMessageView"
|
||||||
|
android:layout_marginTop="@dimen/standard_margin"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginHorizontal="@dimen/standard_margin"
|
||||||
|
android:background="@drawable/shape_grouped_outcoming_message">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/playPauseBtn"
|
||||||
|
style="@style/Widget.AppTheme.Button.IconButton"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="@dimen/standard_margin"
|
||||||
|
android:contentDescription="@string/play_pause_voice_message"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:cornerRadius="@dimen/button_corner_radius"
|
||||||
|
app:icon="@drawable/ic_baseline_play_arrow_voice_message_24"
|
||||||
|
app:iconSize="30dp"
|
||||||
|
app:iconTint="@color/high_emphasis_text"
|
||||||
|
app:rippleColor="#1FFFFFFF" />
|
||||||
|
|
||||||
|
<SeekBar
|
||||||
|
android:id="@+id/seekbar"
|
||||||
|
style="@style/Nextcloud.Material.Outgoing.SeekBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="30dp"
|
||||||
|
android:layout_marginEnd="@dimen/standard_margin"
|
||||||
|
android:thumb="@drawable/voice_message_outgoing_seek_bar_slider"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:progress="50" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<Chronometer
|
<Chronometer
|
||||||
android:id="@+id/audioRecordDuration"
|
android:id="@+id/audioRecordDuration"
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<declare-styleable name="MaterialPreferenceCategory">
|
<declare-styleable name="MicInputCloud">
|
||||||
<attr name="mpc_action" format="string" />
|
<attr name="primaryColor" format="color" />
|
||||||
|
<attr name="pauseIcon" format="reference" />
|
||||||
|
<attr name="playIcon" format="reference" />
|
||||||
</declare-styleable>
|
</declare-styleable>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in New Issue
Block a user