Merge pull request #7448 from vector-im/feature/fre/voice_broadcast_timeline_improvements
Voice Broadcast - Improve timeline rendering code
This commit is contained in:
commit
6f1e0b5bbd
1
changelog.d/7448.wip
Normal file
1
changelog.d/7448.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Voice Broadcast] Improve timeline items factory and handle bad recording state display
|
@ -2,6 +2,7 @@
|
|||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="ellipsis" translatable="false">…</string>
|
<string name="ellipsis" translatable="false">…</string>
|
||||||
|
<string name="no_value_placeholder" translatable="false">–</string>
|
||||||
|
|
||||||
<!-- Temporary string -->
|
<!-- Temporary string -->
|
||||||
<string name="not_implemented" translatable="false">Not implemented yet in ${app_name}</string>
|
<string name="not_implemented" translatable="false">Not implemented yet in ${app_name}</string>
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<declare-styleable name="VoiceBroadcastMetadataView">
|
||||||
|
<attr name="metadataIcon" format="reference" />
|
||||||
|
<attr name="metadataValue" format="string" />
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
|
</resources>
|
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="VoiceBroadcastLiveIndicator" parent="Widget.AppCompat.TextView">
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">20dp</item>
|
||||||
|
<item name="android:backgroundTint">?colorError</item>
|
||||||
|
<item name="android:drawablePadding">4dp</item>
|
||||||
|
<item name="android:ellipsize">end</item>
|
||||||
|
<item name="android:gravity">center_vertical</item>
|
||||||
|
<item name="android:maxWidth">100dp</item>
|
||||||
|
<item name="android:paddingEnd">4dp</item>
|
||||||
|
<item name="android:paddingStart">4dp</item>
|
||||||
|
<item name="android:singleLine">true</item>
|
||||||
|
<item name="android:textColor">?colorOnError</item>
|
||||||
|
<item name="drawableTint">?colorOnError</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
@ -201,7 +201,7 @@ class MessageItemFactory @Inject constructor(
|
|||||||
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes)
|
||||||
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
|
is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes)
|
||||||
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, callback, attributes)
|
is MessageVoiceBroadcastInfoContent -> voiceBroadcastItemFactory.create(params, messageContent, highlight, attributes)
|
||||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||||
}
|
}
|
||||||
return messageItem?.apply {
|
return messageItem?.apply {
|
||||||
|
@ -15,14 +15,13 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.app.features.home.room.detail.timeline.factory
|
package im.vector.app.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyHolder
|
|
||||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
|
||||||
import im.vector.app.core.resources.ColorProvider
|
import im.vector.app.core.resources.ColorProvider
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
import im.vector.app.core.resources.DrawableProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.app.features.displayname.getBestName
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||||
|
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageVoiceBroadcastItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastListeningItem_
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
|
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceBroadcastRecordingItem
|
||||||
@ -34,7 +33,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
|||||||
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
import org.matrix.android.sdk.api.session.getUser
|
import org.matrix.android.sdk.api.session.getUserOrDefault
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -51,81 +50,59 @@ class VoiceBroadcastItemFactory @Inject constructor(
|
|||||||
params: TimelineItemFactoryParams,
|
params: TimelineItemFactoryParams,
|
||||||
messageContent: MessageVoiceBroadcastInfoContent,
|
messageContent: MessageVoiceBroadcastInfoContent,
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
|
||||||
attributes: AbsMessageItem.Attributes,
|
attributes: AbsMessageItem.Attributes,
|
||||||
): VectorEpoxyModel<out VectorEpoxyHolder>? {
|
): AbsMessageVoiceBroadcastItem<*>? {
|
||||||
// Only display item of the initial event with updated data
|
// Only display item of the initial event with updated data
|
||||||
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
|
if (messageContent.voiceBroadcastState != VoiceBroadcastState.STARTED) return null
|
||||||
val eventsGroup = params.eventsGroup ?: return null
|
|
||||||
val voiceBroadcastEventsGroup = VoiceBroadcastEventsGroup(eventsGroup)
|
val voiceBroadcastEventsGroup = params.eventsGroup?.let { VoiceBroadcastEventsGroup(it) } ?: return null
|
||||||
val mostRecentTimelineEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent()
|
val voiceBroadcastEvent = voiceBroadcastEventsGroup.getLastDisplayableEvent().root.asVoiceBroadcastEvent() ?: return null
|
||||||
val mostRecentEvent = mostRecentTimelineEvent.root.asVoiceBroadcastEvent()
|
val voiceBroadcastContent = voiceBroadcastEvent.content ?: return null
|
||||||
val mostRecentMessageContent = mostRecentEvent?.content ?: return null
|
val voiceBroadcastId = voiceBroadcastEventsGroup.voiceBroadcastId
|
||||||
val isRecording = mostRecentMessageContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && mostRecentEvent.root.stateKey == session.myUserId
|
|
||||||
val recorderName = mostRecentTimelineEvent.root.stateKey?.let { session.getUser(it) }?.displayName ?: mostRecentTimelineEvent.root.stateKey
|
val isRecording = voiceBroadcastContent.voiceBroadcastState != VoiceBroadcastState.STOPPED && voiceBroadcastEvent.root.stateKey == session.myUserId
|
||||||
|
|
||||||
|
val voiceBroadcastAttributes = AbsMessageVoiceBroadcastItem.Attributes(
|
||||||
|
voiceBroadcastId = voiceBroadcastId,
|
||||||
|
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
|
||||||
|
recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(),
|
||||||
|
recorder = voiceBroadcastRecorder,
|
||||||
|
player = voiceBroadcastPlayer,
|
||||||
|
roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(),
|
||||||
|
colorProvider = colorProvider,
|
||||||
|
drawableProvider = drawableProvider,
|
||||||
|
)
|
||||||
|
|
||||||
return if (isRecording) {
|
return if (isRecording) {
|
||||||
createRecordingItem(
|
createRecordingItem(highlight, attributes, voiceBroadcastAttributes)
|
||||||
params.event.roomId,
|
|
||||||
eventsGroup.groupId,
|
|
||||||
highlight,
|
|
||||||
callback,
|
|
||||||
attributes
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
createListeningItem(
|
createListeningItem(highlight, attributes, voiceBroadcastAttributes)
|
||||||
params.event.roomId,
|
|
||||||
eventsGroup.groupId,
|
|
||||||
mostRecentMessageContent.voiceBroadcastState,
|
|
||||||
recorderName,
|
|
||||||
highlight,
|
|
||||||
callback,
|
|
||||||
attributes
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRecordingItem(
|
private fun createRecordingItem(
|
||||||
roomId: String,
|
|
||||||
voiceBroadcastId: String,
|
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
|
||||||
attributes: AbsMessageItem.Attributes,
|
attributes: AbsMessageItem.Attributes,
|
||||||
|
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||||
): MessageVoiceBroadcastRecordingItem {
|
): MessageVoiceBroadcastRecordingItem {
|
||||||
val roomSummary = session.getRoom(roomId)?.roomSummary()
|
|
||||||
return MessageVoiceBroadcastRecordingItem_()
|
return MessageVoiceBroadcastRecordingItem_()
|
||||||
.id("voice_broadcast_$voiceBroadcastId")
|
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
|
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.roomItem(roomSummary?.toMatrixItem())
|
|
||||||
.colorProvider(colorProvider)
|
|
||||||
.drawableProvider(drawableProvider)
|
|
||||||
.voiceBroadcastRecorder(voiceBroadcastRecorder)
|
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.callback(callback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createListeningItem(
|
private fun createListeningItem(
|
||||||
roomId: String,
|
|
||||||
voiceBroadcastId: String,
|
|
||||||
voiceBroadcastState: VoiceBroadcastState?,
|
|
||||||
broadcasterName: String?,
|
|
||||||
highlight: Boolean,
|
highlight: Boolean,
|
||||||
callback: TimelineEventController.Callback?,
|
|
||||||
attributes: AbsMessageItem.Attributes,
|
attributes: AbsMessageItem.Attributes,
|
||||||
|
voiceBroadcastAttributes: AbsMessageVoiceBroadcastItem.Attributes,
|
||||||
): MessageVoiceBroadcastListeningItem {
|
): MessageVoiceBroadcastListeningItem {
|
||||||
val roomSummary = session.getRoom(roomId)?.roomSummary()
|
|
||||||
return MessageVoiceBroadcastListeningItem_()
|
return MessageVoiceBroadcastListeningItem_()
|
||||||
.id("voice_broadcast_$voiceBroadcastId")
|
.id("voice_broadcast_${voiceBroadcastAttributes.voiceBroadcastId}")
|
||||||
.attributes(attributes)
|
.attributes(attributes)
|
||||||
|
.voiceBroadcastAttributes(voiceBroadcastAttributes)
|
||||||
.highlighted(highlight)
|
.highlighted(highlight)
|
||||||
.roomItem(roomSummary?.toMatrixItem())
|
|
||||||
.colorProvider(colorProvider)
|
|
||||||
.drawableProvider(drawableProvider)
|
|
||||||
.voiceBroadcastPlayer(voiceBroadcastPlayer)
|
|
||||||
.voiceBroadcastId(voiceBroadcastId)
|
|
||||||
.voiceBroadcastState(voiceBroadcastState)
|
|
||||||
.broadcasterName(broadcasterName)
|
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.callback(callback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +141,9 @@ class CallSignalingEventsGroup(private val group: TimelineEventsGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
|
class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
|
||||||
|
|
||||||
|
val voiceBroadcastId = group.groupId
|
||||||
|
|
||||||
fun getLastDisplayableEvent(): TimelineEvent {
|
fun getLastDisplayableEvent(): TimelineEvent {
|
||||||
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
return group.events.find { it.root.asVoiceBroadcastEvent()?.content?.voiceBroadcastState == VoiceBroadcastState.STOPPED }
|
||||||
?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }
|
?: group.events.filter { it.root.type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO }.maxBy { it.root.originServerTs ?: 0L }
|
||||||
|
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.tintBackground
|
||||||
|
import im.vector.app.core.resources.ColorProvider
|
||||||
|
import im.vector.app.core.resources.DrawableProvider
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
|
||||||
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
||||||
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
|
abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Holder> : AbsMessageItem<H>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
lateinit var voiceBroadcastAttributes: Attributes
|
||||||
|
|
||||||
|
protected val voiceBroadcastId get() = voiceBroadcastAttributes.voiceBroadcastId
|
||||||
|
protected val voiceBroadcastState get() = voiceBroadcastAttributes.voiceBroadcastState
|
||||||
|
protected val recorderName get() = voiceBroadcastAttributes.recorderName
|
||||||
|
protected val recorder get() = voiceBroadcastAttributes.recorder
|
||||||
|
protected val player get() = voiceBroadcastAttributes.player
|
||||||
|
protected val roomItem get() = voiceBroadcastAttributes.roomItem
|
||||||
|
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
|
||||||
|
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
|
||||||
|
protected val avatarRenderer get() = attributes.avatarRenderer
|
||||||
|
protected val callback get() = attributes.callback
|
||||||
|
|
||||||
|
override fun isCacheable(): Boolean = false
|
||||||
|
|
||||||
|
override fun bind(holder: H) {
|
||||||
|
super.bind(holder)
|
||||||
|
renderHeader(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderHeader(holder: H) {
|
||||||
|
with(holder) {
|
||||||
|
roomItem?.let {
|
||||||
|
avatarRenderer.render(it, roomAvatarImageView)
|
||||||
|
titleText.text = it.displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderLiveIndicator(holder)
|
||||||
|
renderMetadata(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderLiveIndicator(holder: H) {
|
||||||
|
with(holder) {
|
||||||
|
when (voiceBroadcastState) {
|
||||||
|
VoiceBroadcastState.STARTED,
|
||||||
|
VoiceBroadcastState.RESUMED -> {
|
||||||
|
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
|
||||||
|
liveIndicator.isVisible = true
|
||||||
|
}
|
||||||
|
VoiceBroadcastState.PAUSED -> {
|
||||||
|
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
|
||||||
|
liveIndicator.isVisible = true
|
||||||
|
}
|
||||||
|
VoiceBroadcastState.STOPPED, null -> {
|
||||||
|
liveIndicator.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun renderMetadata(holder: H)
|
||||||
|
|
||||||
|
abstract class Holder(@IdRes stubId: Int) : AbsMessageItem.Holder(stubId) {
|
||||||
|
val liveIndicator by bind<TextView>(R.id.liveIndicator)
|
||||||
|
val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
||||||
|
val titleText by bind<TextView>(R.id.titleText)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Attributes(
|
||||||
|
val voiceBroadcastId: String,
|
||||||
|
val voiceBroadcastState: VoiceBroadcastState?,
|
||||||
|
val recorderName: String,
|
||||||
|
val recorder: VoiceBroadcastRecorder?,
|
||||||
|
val player: VoiceBroadcastPlayer,
|
||||||
|
val roomItem: MatrixItem?,
|
||||||
|
val colorProvider: ColorProvider,
|
||||||
|
val drawableProvider: DrawableProvider,
|
||||||
|
)
|
||||||
|
}
|
@ -18,56 +18,19 @@ package im.vector.app.features.home.room.detail.timeline.item
|
|||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.extensions.tintBackground
|
|
||||||
import im.vector.app.core.resources.ColorProvider
|
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction
|
import im.vector.app.features.home.room.detail.RoomDetailAction
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastPlayer
|
||||||
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
|
||||||
|
|
||||||
@EpoxyModelClass
|
@EpoxyModelClass
|
||||||
abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceBroadcastListeningItem.Holder>() {
|
abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastListeningItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var callback: TimelineEventController.Callback? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
lateinit var voiceBroadcastId: String
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var voiceBroadcastState: VoiceBroadcastState? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var broadcasterName: String? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
lateinit var colorProvider: ColorProvider
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
lateinit var drawableProvider: DrawableProvider
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var roomItem: MatrixItem? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var title: String? = null
|
|
||||||
|
|
||||||
private lateinit var playerListener: VoiceBroadcastPlayer.Listener
|
private lateinit var playerListener: VoiceBroadcastPlayer.Listener
|
||||||
|
|
||||||
override fun isCacheable(): Boolean = false
|
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
bindVoiceBroadcastItem(holder)
|
bindVoiceBroadcastItem(holder)
|
||||||
@ -75,51 +38,20 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
|
|||||||
|
|
||||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||||
playerListener = VoiceBroadcastPlayer.Listener { state ->
|
playerListener = VoiceBroadcastPlayer.Listener { state ->
|
||||||
renderState(holder, state)
|
renderPlayingState(holder, state)
|
||||||
}
|
}
|
||||||
voiceBroadcastPlayer?.addListener(playerListener)
|
player.addListener(voiceBroadcastId, playerListener)
|
||||||
renderHeader(holder)
|
|
||||||
renderLiveIcon(holder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderHeader(holder: Holder) {
|
override fun renderMetadata(holder: Holder) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
roomItem?.let {
|
broadcasterNameMetadata.value = recorderName
|
||||||
attributes.avatarRenderer.render(it, roomAvatarImageView)
|
voiceBroadcastMetadata.isVisible = true
|
||||||
titleText.text = it.displayName
|
listenersCountMetadata.isVisible = false
|
||||||
}
|
|
||||||
broadcasterNameText.text = broadcasterName
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderLiveIcon(holder: Holder) {
|
private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
||||||
with(holder) {
|
|
||||||
when (voiceBroadcastState) {
|
|
||||||
VoiceBroadcastState.STARTED,
|
|
||||||
VoiceBroadcastState.RESUMED -> {
|
|
||||||
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
|
|
||||||
liveIndicator.isVisible = true
|
|
||||||
}
|
|
||||||
VoiceBroadcastState.PAUSED -> {
|
|
||||||
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
|
|
||||||
liveIndicator.isVisible = true
|
|
||||||
}
|
|
||||||
VoiceBroadcastState.STOPPED, null -> {
|
|
||||||
liveIndicator.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renderState(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
|
||||||
if (isCurrentMediaActive()) {
|
|
||||||
renderActiveMedia(holder, state)
|
|
||||||
} else {
|
|
||||||
renderInactiveMedia(holder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renderActiveMedia(holder: Holder, state: VoiceBroadcastPlayer.State) {
|
|
||||||
with(holder) {
|
with(holder) {
|
||||||
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING
|
||||||
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
playPauseButton.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING
|
||||||
@ -127,15 +59,15 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
|
|||||||
when (state) {
|
when (state) {
|
||||||
VoiceBroadcastPlayer.State.PLAYING -> {
|
VoiceBroadcastPlayer.State.PLAYING -> {
|
||||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
playPauseButton.setImageResource(R.drawable.ic_play_pause_pause)
|
||||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
||||||
playPauseButton.onClick { attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
|
playPauseButton.onClick { callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.Pause) }
|
||||||
}
|
}
|
||||||
VoiceBroadcastPlayer.State.IDLE,
|
VoiceBroadcastPlayer.State.IDLE,
|
||||||
VoiceBroadcastPlayer.State.PAUSED -> {
|
VoiceBroadcastPlayer.State.PAUSED -> {
|
||||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
||||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast)
|
||||||
playPauseButton.onClick {
|
playPauseButton.onClick {
|
||||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
|
callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VoiceBroadcastPlayer.State.BUFFERING -> Unit
|
VoiceBroadcastPlayer.State.BUFFERING -> Unit
|
||||||
@ -143,34 +75,19 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageItem<MessageVoiceB
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderInactiveMedia(holder: Holder) {
|
|
||||||
with(holder) {
|
|
||||||
bufferingView.isVisible = false
|
|
||||||
playPauseButton.isVisible = true
|
|
||||||
playPauseButton.setImageResource(R.drawable.ic_play_pause_play)
|
|
||||||
playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast)
|
|
||||||
playPauseButton.onClick {
|
|
||||||
attributes.callback?.onTimelineItemAction(RoomDetailAction.VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcastId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isCurrentMediaActive() = voiceBroadcastPlayer?.currentVoiceBroadcastId == voiceBroadcastId
|
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
super.unbind(holder)
|
super.unbind(holder)
|
||||||
voiceBroadcastPlayer?.removeListener(playerListener)
|
player.removeListener(voiceBroadcastId, playerListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
|
||||||
val liveIndicator by bind<TextView>(R.id.liveIndicator)
|
|
||||||
val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
|
||||||
val titleText by bind<TextView>(R.id.titleText)
|
|
||||||
val playPauseButton by bind<ImageButton>(R.id.playPauseButton)
|
val playPauseButton by bind<ImageButton>(R.id.playPauseButton)
|
||||||
val bufferingView by bind<View>(R.id.bufferingView)
|
val bufferingView by bind<View>(R.id.bufferingView)
|
||||||
val broadcasterNameText by bind<TextView>(R.id.broadcasterNameText)
|
val broadcasterNameMetadata by bind<VoiceBroadcastMetadataView>(R.id.broadcasterNameMetadata)
|
||||||
|
val voiceBroadcastMetadata by bind<VoiceBroadcastMetadataView>(R.id.voiceBroadcastMetadata)
|
||||||
|
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -17,45 +17,19 @@
|
|||||||
package im.vector.app.features.home.room.detail.timeline.item
|
package im.vector.app.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.epoxy.onClick
|
import im.vector.app.core.epoxy.onClick
|
||||||
import im.vector.app.core.extensions.tintBackground
|
|
||||||
import im.vector.app.core.resources.ColorProvider
|
|
||||||
import im.vector.app.core.resources.DrawableProvider
|
|
||||||
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
|
||||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
|
||||||
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
|
||||||
|
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
|
|
||||||
@EpoxyModelClass
|
@EpoxyModelClass
|
||||||
abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceBroadcastRecordingItem.Holder>() {
|
abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastRecordingItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
private var recorderListener: VoiceBroadcastRecorder.Listener? = null
|
||||||
var callback: TimelineEventController.Callback? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
lateinit var colorProvider: ColorProvider
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
lateinit var drawableProvider: DrawableProvider
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var roomItem: MatrixItem? = null
|
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
var title: String? = null
|
|
||||||
|
|
||||||
private lateinit var recorderListener: VoiceBroadcastRecorder.Listener
|
|
||||||
|
|
||||||
override fun isCacheable(): Boolean = false
|
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
@ -63,73 +37,80 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceB
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun bindVoiceBroadcastItem(holder: Holder) {
|
private fun bindVoiceBroadcastItem(holder: Holder) {
|
||||||
|
if (recorder != null && recorder?.state != VoiceBroadcastRecorder.State.Idle) {
|
||||||
recorderListener = object : VoiceBroadcastRecorder.Listener {
|
recorderListener = object : VoiceBroadcastRecorder.Listener {
|
||||||
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
override fun onStateUpdated(state: VoiceBroadcastRecorder.State) {
|
||||||
renderState(holder, state)
|
renderRecordingState(holder, state)
|
||||||
}
|
|
||||||
}
|
|
||||||
voiceBroadcastRecorder?.addListener(recorderListener)
|
|
||||||
renderHeader(holder)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun renderHeader(holder: Holder) {
|
|
||||||
with(holder) {
|
|
||||||
roomItem?.let {
|
|
||||||
attributes.avatarRenderer.render(it, roomAvatarImageView)
|
|
||||||
titleText.text = it.displayName
|
|
||||||
}
|
}
|
||||||
|
}.also { recorder?.addListener(it) }
|
||||||
|
} else {
|
||||||
|
renderVoiceBroadcastState(holder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) {
|
override fun renderMetadata(holder: Holder) {
|
||||||
with(holder) {
|
with(holder) {
|
||||||
|
listenersCountMetadata.isVisible = false
|
||||||
|
remainingTimeMetadata.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) {
|
||||||
when (state) {
|
when (state) {
|
||||||
VoiceBroadcastRecorder.State.Recording -> {
|
VoiceBroadcastRecorder.State.Recording -> renderRecordingState(holder)
|
||||||
|
VoiceBroadcastRecorder.State.Paused -> renderPausedState(holder)
|
||||||
|
VoiceBroadcastRecorder.State.Idle -> renderStoppedState(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderVoiceBroadcastState(holder: Holder) {
|
||||||
|
when (voiceBroadcastState) {
|
||||||
|
VoiceBroadcastState.STARTED,
|
||||||
|
VoiceBroadcastState.RESUMED -> renderRecordingState(holder)
|
||||||
|
VoiceBroadcastState.PAUSED -> renderPausedState(holder)
|
||||||
|
VoiceBroadcastState.STOPPED,
|
||||||
|
null -> renderStoppedState(holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderRecordingState(holder: Holder) = with(holder) {
|
||||||
stopRecordButton.isEnabled = true
|
stopRecordButton.isEnabled = true
|
||||||
recordButton.isEnabled = true
|
recordButton.isEnabled = true
|
||||||
|
|
||||||
liveIndicator.isVisible = true
|
|
||||||
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.colorError))
|
|
||||||
|
|
||||||
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
|
val drawableColor = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)
|
||||||
val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
|
val drawable = drawableProvider.getDrawable(R.drawable.ic_play_pause_pause, drawableColor)
|
||||||
recordButton.setImageDrawable(drawable)
|
recordButton.setImageDrawable(drawable)
|
||||||
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
|
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_pause_voice_broadcast_record)
|
||||||
recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
|
recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Pause) }
|
||||||
stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
||||||
}
|
}
|
||||||
VoiceBroadcastRecorder.State.Paused -> {
|
|
||||||
|
private fun renderPausedState(holder: Holder) = with(holder) {
|
||||||
stopRecordButton.isEnabled = true
|
stopRecordButton.isEnabled = true
|
||||||
recordButton.isEnabled = true
|
recordButton.isEnabled = true
|
||||||
|
|
||||||
liveIndicator.isVisible = true
|
|
||||||
liveIndicator.tintBackground(colorProvider.getColorFromAttribute(R.attr.vctr_content_quaternary))
|
|
||||||
|
|
||||||
recordButton.setImageResource(R.drawable.ic_recording_dot)
|
recordButton.setImageResource(R.drawable.ic_recording_dot)
|
||||||
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
|
recordButton.contentDescription = holder.view.resources.getString(R.string.a11y_resume_voice_broadcast_record)
|
||||||
recordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
|
recordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
|
||||||
stopRecordButton.onClick { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
stopRecordButton.onClick { callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Stop) }
|
||||||
}
|
}
|
||||||
VoiceBroadcastRecorder.State.Idle -> {
|
|
||||||
|
private fun renderStoppedState(holder: Holder) = with(holder) {
|
||||||
recordButton.isEnabled = false
|
recordButton.isEnabled = false
|
||||||
stopRecordButton.isEnabled = false
|
stopRecordButton.isEnabled = false
|
||||||
liveIndicator.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun unbind(holder: Holder) {
|
override fun unbind(holder: Holder) {
|
||||||
super.unbind(holder)
|
super.unbind(holder)
|
||||||
voiceBroadcastRecorder?.removeListener(recorderListener)
|
recorderListener?.let { recorder?.removeListener(it) }
|
||||||
|
recorderListener = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getViewStubId() = STUB_ID
|
override fun getViewStubId() = STUB_ID
|
||||||
|
|
||||||
class Holder : AbsMessageItem.Holder(STUB_ID) {
|
class Holder : AbsMessageVoiceBroadcastItem.Holder(STUB_ID) {
|
||||||
val liveIndicator by bind<TextView>(R.id.liveIndicator)
|
val listenersCountMetadata by bind<VoiceBroadcastMetadataView>(R.id.listenersCountMetadata)
|
||||||
val roomAvatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
|
val remainingTimeMetadata by bind<VoiceBroadcastMetadataView>(R.id.remainingTimeMetadata)
|
||||||
val titleText by bind<TextView>(R.id.titleText)
|
|
||||||
val recordButton by bind<ImageButton>(R.id.recordButton)
|
val recordButton by bind<ImageButton>(R.id.recordButton)
|
||||||
val stopRecordButton by bind<ImageButton>(R.id.stopRecordButton)
|
val stopRecordButton by bind<ImageButton>(R.id.stopRecordButton)
|
||||||
}
|
}
|
||||||
|
@ -82,10 +82,17 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
set(value) {
|
set(value) {
|
||||||
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
Timber.w("## VoiceBroadcastPlayer state: $field -> $value")
|
||||||
field = value
|
field = value
|
||||||
listeners.forEach { it.onStateChanged(value) }
|
// Notify state change to all the listeners attached to the current voice broadcast id
|
||||||
|
currentVoiceBroadcastId?.let { voiceBroadcastId ->
|
||||||
|
listeners[voiceBroadcastId]?.forEach { listener -> listener.onStateChanged(value) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private var currentRoomId: String? = null
|
private var currentRoomId: String? = null
|
||||||
private var listeners = CopyOnWriteArrayList<Listener>()
|
|
||||||
|
/**
|
||||||
|
* Map voiceBroadcastId to listeners.
|
||||||
|
*/
|
||||||
|
private val listeners: MutableMap<String, CopyOnWriteArrayList<Listener>> = mutableMapOf()
|
||||||
|
|
||||||
fun playOrResume(roomId: String, eventId: String) {
|
fun playOrResume(roomId: String, eventId: String) {
|
||||||
val hasChanged = currentVoiceBroadcastId != eventId
|
val hasChanged = currentVoiceBroadcastId != eventId
|
||||||
@ -133,13 +140,21 @@ class VoiceBroadcastPlayer @Inject constructor(
|
|||||||
currentVoiceBroadcastId = null
|
currentVoiceBroadcastId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addListener(listener: Listener) {
|
/**
|
||||||
listeners.add(listener)
|
* Add a [Listener] to the given voice broadcast id.
|
||||||
listener.onStateChanged(state)
|
*/
|
||||||
|
fun addListener(voiceBroadcastId: String, listener: Listener) {
|
||||||
|
listeners[voiceBroadcastId]?.add(listener) ?: run {
|
||||||
|
listeners[voiceBroadcastId] = CopyOnWriteArrayList<Listener>().apply { add(listener) }
|
||||||
|
}
|
||||||
|
if (voiceBroadcastId == currentVoiceBroadcastId) listener.onStateChanged(state) else listener.onStateChanged(State.IDLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeListener(listener: Listener) {
|
/**
|
||||||
listeners.remove(listener)
|
* Remove a [Listener] from the given voice broadcast id.
|
||||||
|
*/
|
||||||
|
fun removeListener(voiceBroadcastId: String, listener: Listener) {
|
||||||
|
listeners[voiceBroadcastId]?.remove(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startPlayback(roomId: String, eventId: String) {
|
private fun startPlayback(roomId: String, eventId: String) {
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 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.app.features.voicebroadcast.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.core.content.res.use
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.databinding.ViewVoiceBroadcastMetadataBinding
|
||||||
|
|
||||||
|
class VoiceBroadcastMetadataView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
private val views = ViewVoiceBroadcastMetadataBinding.inflate(
|
||||||
|
LayoutInflater.from(context),
|
||||||
|
this
|
||||||
|
)
|
||||||
|
|
||||||
|
var value: String
|
||||||
|
get() = views.metadataValue.text.toString()
|
||||||
|
set(newValue) {
|
||||||
|
views.metadataValue.text = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
context.obtainStyledAttributes(
|
||||||
|
attrs,
|
||||||
|
R.styleable.VoiceBroadcastMetadataView,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
).use {
|
||||||
|
setIcon(it)
|
||||||
|
setValue(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setIcon(typedArray: TypedArray) {
|
||||||
|
val icon = typedArray.getDrawable(R.styleable.VoiceBroadcastMetadataView_metadataIcon)
|
||||||
|
views.metadataIcon.setImageDrawable(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setValue(typedArray: TypedArray) {
|
||||||
|
val value = typedArray.getString(R.styleable.VoiceBroadcastMetadataView_metadataValue)
|
||||||
|
views.metadataValue.text = value
|
||||||
|
}
|
||||||
|
}
|
9
vector/src/main/res/drawable/ic_timer.xml
Normal file
9
vector/src/main/res/drawable/ic_timer.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M10,1H6V2.333H10V1ZM7.333,9.667H8.667V5.667H7.333V9.667ZM12.687,5.26L13.633,4.313C13.347,3.973 13.033,3.653 12.693,3.373L11.747,4.32C10.713,3.493 9.413,3 8,3C4.687,3 2,5.687 2,9C2,12.313 4.68,15 8,15C11.32,15 14,12.313 14,9C14,7.587 13.507,6.287 12.687,5.26ZM8,13.667C5.42,13.667 3.333,11.58 3.333,9C3.333,6.42 5.42,4.333 8,4.333C10.58,4.333 12.667,6.42 12.667,9C12.667,11.58 10.58,13.667 8,13.667Z"
|
||||||
|
android:fillColor="#737D8C"/>
|
||||||
|
</vector>
|
12
vector/src/main/res/drawable/ic_voice_broadcast_mic.xml
Normal file
12
vector/src/main/res/drawable/ic_voice_broadcast_mic.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="16dp"
|
||||||
|
android:height="16dp"
|
||||||
|
android:viewportWidth="16"
|
||||||
|
android:viewportHeight="16">
|
||||||
|
<path
|
||||||
|
android:pathData="M5.4,4.1C5.4,2.664 6.564,1.5 8,1.5C9.436,1.5 10.6,2.664 10.6,4.1V7.988C10.6,9.424 9.436,10.588 8,10.588C6.564,10.588 5.4,9.424 5.4,7.988V4.1Z"
|
||||||
|
android:fillColor="#737D8C"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M3.45,7.158C3.91,7.158 4.283,7.531 4.283,7.992C4.283,10.037 5.941,11.697 7.99,11.703C7.993,11.703 7.996,11.703 8,11.703C8.003,11.703 8.006,11.703 8.01,11.703C10.059,11.697 11.716,10.037 11.716,7.992C11.716,7.531 12.089,7.158 12.55,7.158C13.01,7.158 13.383,7.531 13.383,7.992C13.383,10.679 11.41,12.905 8.833,13.305V13.834C8.833,14.294 8.46,14.667 8,14.667C7.539,14.667 7.166,14.294 7.166,13.834V13.305C4.59,12.905 2.616,10.679 2.616,7.992C2.616,7.531 2.989,7.158 3.45,7.158Z"
|
||||||
|
android:fillColor="#737D8C"/>
|
||||||
|
</vector>
|
@ -7,25 +7,14 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/rounded_rect_shape_8"
|
android:background="@drawable/rounded_rect_shape_8"
|
||||||
android:backgroundTint="?vctr_content_quinary"
|
android:backgroundTint="?vctr_content_quinary"
|
||||||
android:padding="@dimen/layout_vertical_margin"
|
android:padding="@dimen/layout_vertical_margin">
|
||||||
tools:viewBindingIgnore="true">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/liveIndicator"
|
android:id="@+id/liveIndicator"
|
||||||
android:layout_width="wrap_content"
|
style="@style/VoiceBroadcastLiveIndicator"
|
||||||
android:layout_height="20dp"
|
|
||||||
android:background="@drawable/rounded_rect_shape_2"
|
android:background="@drawable/rounded_rect_shape_2"
|
||||||
android:backgroundTint="?colorError"
|
|
||||||
android:drawablePadding="4dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:maxWidth="100dp"
|
|
||||||
android:paddingHorizontal="4dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/voice_broadcast_live"
|
android:text="@string/voice_broadcast_live"
|
||||||
android:textColor="?colorOnError"
|
app:drawableStartCompat="@drawable/ic_voice_broadcast"
|
||||||
app:drawableStartCompat="@drawable/ic_voice_broadcast_16"
|
|
||||||
app:drawableTint="?colorOnError"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
@ -54,61 +43,41 @@
|
|||||||
android:contentDescription="@string/avatar"
|
android:contentDescription="@string/avatar"
|
||||||
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:src="@sample/rooms.json/data/name" />
|
tools:text="@sample/rooms.json/data/name" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
android:id="@+id/broadcasterViewGroup"
|
android:id="@+id/metadataFlow"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:gravity="center_vertical"
|
android:orientation="vertical"
|
||||||
android:orientation="horizontal"
|
app:constraint_referenced_ids="broadcasterNameMetadata,voiceBroadcastMetadata,listenersCountMetadata"
|
||||||
|
app:flow_horizontalAlign="start"
|
||||||
|
app:flow_verticalGap="4dp"
|
||||||
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||||
app:layout_constraintTop_toBottomOf="@id/titleText">
|
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
||||||
|
|
||||||
<ImageView
|
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
android:id="@+id/broadcasterIcon"
|
android:id="@+id/broadcasterNameMetadata"
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:layout_marginEnd="5dp"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_microphone"
|
|
||||||
app:tint="?vctr_content_secondary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/broadcasterNameText"
|
|
||||||
style="@style/Widget.Vector.TextView.Caption"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:text="@sample/users.json/data/displayName" />
|
app:metadataIcon="@drawable/ic_voice_broadcast_mic"
|
||||||
</LinearLayout>
|
tools:metadataValue="@sample/users.json/data/displayName" />
|
||||||
|
|
||||||
<LinearLayout
|
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
android:id="@+id/voiceBroadcastViewGroup"
|
android:id="@+id/voiceBroadcastMetadata"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
app:metadataIcon="@drawable/ic_voice_broadcast"
|
||||||
android:gravity="center_vertical"
|
app:metadataValue="@string/attachment_type_voice_broadcast" />
|
||||||
android:orientation="horizontal"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/broadcasterViewGroup">
|
|
||||||
|
|
||||||
<ImageView
|
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
android:id="@+id/voiceBroadcastIcon"
|
android:id="@+id/listenersCountMetadata"
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:layout_marginEnd="5dp"
|
|
||||||
android:contentDescription="@null"
|
|
||||||
android:src="@drawable/ic_voice_broadcast_16"
|
|
||||||
app:tint="?vctr_content_secondary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/voiceBroadcastText"
|
|
||||||
style="@style/Widget.Vector.TextView.Caption"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/attachment_type_voice_broadcast" />
|
app:metadataIcon="@drawable/ic_member_small"
|
||||||
</LinearLayout>
|
app:metadataValue="@string/no_value_placeholder"
|
||||||
|
tools:metadataValue="5 listeners" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/headerBottomBarrier"
|
android:id="@+id/headerBottomBarrier"
|
||||||
@ -116,7 +85,16 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="bottom"
|
app:barrierDirection="bottom"
|
||||||
app:barrierMargin="12dp"
|
app:barrierMargin="12dp"
|
||||||
app:constraint_referenced_ids="roomAvatarImageView,titleText,broadcasterViewGroup,voiceBroadcastViewGroup" />
|
app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/controllerButtonsFlow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
app:constraint_referenced_ids="playPauseButton,bufferingView"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/playPauseButton"
|
android:id="@+id/playPauseButton"
|
||||||
@ -126,24 +104,14 @@
|
|||||||
android:backgroundTint="?vctr_system"
|
android:backgroundTint="?vctr_system"
|
||||||
android:contentDescription="@string/a11y_play_voice_broadcast"
|
android:contentDescription="@string/a11y_play_voice_broadcast"
|
||||||
android:src="@drawable/ic_play_pause_play"
|
android:src="@drawable/ic_play_pause_play"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier"
|
|
||||||
app:tint="?vctr_content_secondary" />
|
app:tint="?vctr_content_secondary" />
|
||||||
|
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/bufferingView"
|
android:id="@+id/bufferingView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:contentDescription="@string/a11y_voice_broadcast_buffering"
|
android:contentDescription="@string/a11y_voice_broadcast_buffering"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
android:indeterminateTint="?vctr_content_secondary"
|
android:indeterminateTint="?vctr_content_secondary" />
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -7,25 +7,14 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/rounded_rect_shape_8"
|
android:background="@drawable/rounded_rect_shape_8"
|
||||||
android:backgroundTint="?vctr_content_quinary"
|
android:backgroundTint="?vctr_content_quinary"
|
||||||
android:padding="@dimen/layout_vertical_margin"
|
android:padding="@dimen/layout_vertical_margin">
|
||||||
tools:viewBindingIgnore="true">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/liveIndicator"
|
android:id="@+id/liveIndicator"
|
||||||
android:layout_width="wrap_content"
|
style="@style/VoiceBroadcastLiveIndicator"
|
||||||
android:layout_height="20dp"
|
|
||||||
android:background="@drawable/rounded_rect_shape_2"
|
android:background="@drawable/rounded_rect_shape_2"
|
||||||
android:backgroundTint="?colorError"
|
|
||||||
android:drawablePadding="4dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:maxWidth="100dp"
|
|
||||||
android:paddingHorizontal="4dp"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:text="@string/voice_broadcast_live"
|
android:text="@string/voice_broadcast_live"
|
||||||
android:textColor="?colorOnError"
|
app:drawableStartCompat="@drawable/ic_voice_broadcast"
|
||||||
app:drawableStartCompat="@drawable/ic_voice_broadcast_16"
|
|
||||||
app:drawableTint="?colorOnError"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
@ -54,7 +43,34 @@
|
|||||||
android:contentDescription="@string/avatar"
|
android:contentDescription="@string/avatar"
|
||||||
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:src="@sample/users.json/data/displayName" />
|
tools:text="@sample/users.json/data/displayName" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/metadataFlow"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:constraint_referenced_ids="listenersCountMetadata,remainingTimeMetadata"
|
||||||
|
app:flow_horizontalAlign="start"
|
||||||
|
app:flow_verticalGap="4dp"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/avatarRightBarrier"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
||||||
|
|
||||||
|
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
|
android:id="@+id/listenersCountMetadata"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:metadataIcon="@drawable/ic_member_small"
|
||||||
|
app:metadataValue="@string/no_value_placeholder"
|
||||||
|
tools:metadataValue="5 listening" />
|
||||||
|
|
||||||
|
<im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
|
||||||
|
android:id="@+id/remainingTimeMetadata"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:metadataIcon="@drawable/ic_timer"
|
||||||
|
tools:metadataValue="3h 2m 50s left" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/headerBottomBarrier"
|
android:id="@+id/headerBottomBarrier"
|
||||||
@ -62,7 +78,16 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="bottom"
|
app:barrierDirection="bottom"
|
||||||
app:barrierMargin="12dp"
|
app:barrierMargin="12dp"
|
||||||
app:constraint_referenced_ids="roomAvatarImageView,titleText" />
|
app:constraint_referenced_ids="roomAvatarImageView,titleText,metadataFlow" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.helper.widget.Flow
|
||||||
|
android:id="@+id/controllerButtonsFlow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
app:constraint_referenced_ids="recordButton,stopRecordButton"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/recordButton"
|
android:id="@+id/recordButton"
|
||||||
@ -71,11 +96,7 @@
|
|||||||
android:background="@drawable/bg_rounded_button"
|
android:background="@drawable/bg_rounded_button"
|
||||||
android:backgroundTint="?vctr_system"
|
android:backgroundTint="?vctr_system"
|
||||||
android:contentDescription="@string/a11y_resume_voice_broadcast_record"
|
android:contentDescription="@string/a11y_resume_voice_broadcast_record"
|
||||||
android:src="@drawable/ic_recording_dot"
|
android:src="@drawable/ic_recording_dot" />
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/stopRecordButton"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/headerBottomBarrier" />
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/stopRecordButton"
|
android:id="@+id/stopRecordButton"
|
||||||
@ -84,10 +105,6 @@
|
|||||||
android:background="@drawable/bg_rounded_button"
|
android:background="@drawable/bg_rounded_button"
|
||||||
android:backgroundTint="?vctr_system"
|
android:backgroundTint="?vctr_system"
|
||||||
android:contentDescription="@string/a11y_stop_voice_broadcast_record"
|
android:contentDescription="@string/a11y_stop_voice_broadcast_record"
|
||||||
android:src="@drawable/ic_stop"
|
android:src="@drawable/ic_stop" />
|
||||||
app:layout_constraintBottom_toBottomOf="@id/recordButton"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/recordButton"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/recordButton" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
27
vector/src/main/res/layout/view_voice_broadcast_metadata.xml
Normal file
27
vector/src/main/res/layout/view_voice_broadcast_metadata.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:parentTag="android.widget.LinearLayout">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/metadataIcon"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_marginEnd="4dp"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
app:tint="?vctr_content_secondary"
|
||||||
|
tools:src="@drawable/ic_voice_broadcast" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/metadata_value"
|
||||||
|
style="@style/Widget.Vector.TextView.Caption"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/no_value_placeholder"
|
||||||
|
tools:text="@string/attachment_type_voice_broadcast" />
|
||||||
|
</merge>
|
Loading…
Reference in New Issue
Block a user