Timeline - Add abstraction on voice broadcast items

This commit is contained in:
Florian Renaud 2022-10-24 14:10:51 +02:00
parent 6a88c61d12
commit 1566adb669
14 changed files with 329 additions and 216 deletions

View File

@ -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>

View File

@ -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>

View File

@ -67,6 +67,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
createRecordingItem( createRecordingItem(
params.event.roomId, params.event.roomId,
eventsGroup.groupId, eventsGroup.groupId,
mostRecentMessageContent.voiceBroadcastState,
highlight, highlight,
callback, callback,
attributes attributes
@ -87,6 +88,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
private fun createRecordingItem( private fun createRecordingItem(
roomId: String, roomId: String,
voiceBroadcastId: String, voiceBroadcastId: String,
voiceBroadcastState: VoiceBroadcastState?,
highlight: Boolean, highlight: Boolean,
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
@ -100,6 +102,8 @@ class VoiceBroadcastItemFactory @Inject constructor(
.colorProvider(colorProvider) .colorProvider(colorProvider)
.drawableProvider(drawableProvider) .drawableProvider(drawableProvider)
.voiceBroadcastRecorder(voiceBroadcastRecorder) .voiceBroadcastRecorder(voiceBroadcastRecorder)
.voiceBroadcastId(voiceBroadcastId)
.voiceBroadcastState(voiceBroadcastState)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback) .callback(callback)
} }

View File

@ -0,0 +1,96 @@
/*
* 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.home.room.detail.timeline.TimelineEventController
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
var callback: TimelineEventController.Callback? = null
@EpoxyAttribute
lateinit var colorProvider: ColorProvider
@EpoxyAttribute
lateinit var drawableProvider: DrawableProvider
@EpoxyAttribute
lateinit var voiceBroadcastId: String
@EpoxyAttribute
var voiceBroadcastState: VoiceBroadcastState? = null
@EpoxyAttribute
var roomItem: MatrixItem? = null
override fun isCacheable(): Boolean = false
override fun bind(holder: H) {
super.bind(holder)
renderHeader(holder)
}
private fun renderHeader(holder: H) {
with(holder) {
roomItem?.let {
attributes.avatarRenderer.render(it, roomAvatarImageView)
titleText.text = it.displayName
}
}
renderLiveIcon(holder)
renderMetadata(holder)
}
private fun renderLiveIcon(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)
}
}

View File

@ -18,56 +18,26 @@ 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.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 @EpoxyAttribute
var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null var voiceBroadcastPlayer: VoiceBroadcastPlayer? = null
@EpoxyAttribute
lateinit var voiceBroadcastId: String
@EpoxyAttribute
var voiceBroadcastState: VoiceBroadcastState? = null
@EpoxyAttribute @EpoxyAttribute
var broadcasterName: String? = null 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 +45,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) voiceBroadcastPlayer?.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 = broadcasterName.orEmpty()
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
@ -143,34 +82,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) voiceBroadcastPlayer?.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 {

View File

@ -17,46 +17,23 @@
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.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.views.VoiceBroadcastMetadataView
@EpoxyModelClass @EpoxyModelClass
abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceBroadcastRecordingItem.Holder>() { abstract class MessageVoiceBroadcastRecordingItem : AbsMessageVoiceBroadcastItem<MessageVoiceBroadcastRecordingItem.Holder>() {
@EpoxyAttribute
var callback: TimelineEventController.Callback? = null
@EpoxyAttribute @EpoxyAttribute
var voiceBroadcastRecorder: VoiceBroadcastRecorder? = null 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 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)
bindVoiceBroadcastItem(holder) bindVoiceBroadcastItem(holder)
@ -65,32 +42,26 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceB
private fun bindVoiceBroadcastItem(holder: Holder) { private fun bindVoiceBroadcastItem(holder: Holder) {
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) voiceBroadcastRecorder?.addListener(recorderListener)
renderHeader(holder)
} }
private fun renderHeader(holder: Holder) { override fun renderMetadata(holder: Holder) {
with(holder) { with(holder) {
roomItem?.let { listenersCountMetadata.isVisible = false
attributes.avatarRenderer.render(it, roomAvatarImageView) remainingTimeMetadata.isVisible = false
titleText.text = it.displayName
}
} }
} }
private fun renderState(holder: Holder, state: VoiceBroadcastRecorder.State) { private fun renderRecordingState(holder: Holder, state: VoiceBroadcastRecorder.State) {
with(holder) { with(holder) {
when (state) { when (state) {
VoiceBroadcastRecorder.State.Recording -> { VoiceBroadcastRecorder.State.Recording -> {
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)
@ -102,9 +73,6 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceB
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 { attributes.callback?.onTimelineItemAction(VoiceBroadcastAction.Recording.Resume) }
@ -113,7 +81,6 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceB
VoiceBroadcastRecorder.State.Idle -> { VoiceBroadcastRecorder.State.Idle -> {
recordButton.isEnabled = false recordButton.isEnabled = false
stopRecordButton.isEnabled = false stopRecordButton.isEnabled = false
liveIndicator.isVisible = false
} }
} }
} }
@ -126,10 +93,9 @@ abstract class MessageVoiceBroadcastRecordingItem : AbsMessageItem<MessageVoiceB
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)
} }

View File

@ -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 var 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) {

View File

@ -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
}
}

View 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>

View File

@ -1,21 +0,0 @@
<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="M13.459,2.791C13.233,2.5 12.814,2.448 12.523,2.674C12.233,2.9 12.181,3.318 12.406,3.609L12.406,3.609L12.407,3.61L12.416,3.622C12.425,3.634 12.439,3.654 12.458,3.68C12.496,3.733 12.552,3.815 12.62,3.923C12.757,4.138 12.943,4.456 13.128,4.854C13.502,5.654 13.866,6.756 13.866,8C13.866,9.245 13.502,10.347 13.128,11.147C12.943,11.545 12.757,11.863 12.62,12.078C12.552,12.186 12.496,12.267 12.458,12.321C12.439,12.347 12.425,12.367 12.416,12.378L12.407,12.391L12.406,12.391L12.406,12.392C12.181,12.683 12.233,13.101 12.523,13.327C12.814,13.553 13.233,13.5 13.459,13.21L12.962,12.823C13.459,13.21 13.459,13.21 13.459,13.21L13.46,13.208L13.462,13.205L13.468,13.198L13.485,13.175C13.5,13.155 13.52,13.128 13.545,13.093C13.595,13.023 13.664,12.922 13.745,12.794C13.908,12.538 14.123,12.17 14.337,11.711C14.763,10.797 15.199,9.499 15.199,8C15.199,6.502 14.763,5.204 14.337,4.29C14.123,3.831 13.908,3.463 13.745,3.207C13.664,3.079 13.595,2.978 13.545,2.908C13.52,2.873 13.5,2.846 13.485,2.826L13.468,2.803L13.462,2.795L13.46,2.793L13.46,2.792C13.46,2.792 13.459,2.791 12.933,3.2L13.459,2.791Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M11.726,5.191C11.5,4.901 11.081,4.848 10.791,5.074C10.501,5.3 10.448,5.717 10.672,6.008L10.674,6.011C10.677,6.015 10.683,6.022 10.691,6.033C10.707,6.056 10.731,6.092 10.762,6.141C10.825,6.238 10.91,6.384 10.996,6.568C11.169,6.94 11.333,7.442 11.333,8.001C11.333,8.559 11.169,9.061 10.996,9.433C10.91,9.617 10.825,9.763 10.762,9.86C10.731,9.909 10.707,9.945 10.691,9.968C10.683,9.979 10.677,9.986 10.674,9.99L10.672,9.994C10.448,10.284 10.501,10.701 10.791,10.927C11.081,11.153 11.5,11.101 11.726,10.81L11.2,10.401C11.726,10.81 11.726,10.81 11.726,10.81L11.727,10.808L11.729,10.806L11.733,10.801L11.744,10.787C11.752,10.775 11.764,10.759 11.778,10.74C11.806,10.7 11.843,10.646 11.887,10.576C11.975,10.438 12.09,10.241 12.204,9.997C12.431,9.511 12.667,8.813 12.667,8.001C12.667,7.188 12.431,6.49 12.204,6.004C12.09,5.76 11.975,5.563 11.887,5.425C11.843,5.356 11.806,5.301 11.778,5.261C11.764,5.242 11.752,5.226 11.744,5.214L11.733,5.2L11.729,5.195L11.727,5.193L11.727,5.192C11.727,5.192 11.726,5.191 11.2,5.601L11.726,5.191Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M2.407,13.21C2.633,13.5 3.052,13.553 3.343,13.327C3.633,13.101 3.686,12.683 3.461,12.392L3.46,12.391L3.459,12.391L3.45,12.378C3.441,12.366 3.427,12.347 3.408,12.321C3.371,12.267 3.314,12.186 3.246,12.078C3.109,11.863 2.924,11.545 2.738,11.147C2.364,10.347 2,9.245 2,8C2,6.756 2.364,5.654 2.738,4.854C2.924,4.456 3.109,4.138 3.246,3.923C3.314,3.815 3.371,3.733 3.408,3.68C3.427,3.654 3.441,3.634 3.45,3.622L3.459,3.61L3.46,3.609L3.461,3.609C3.686,3.318 3.633,2.9 3.343,2.674C3.052,2.448 2.633,2.5 2.407,2.791L2.904,3.177C2.407,2.791 2.407,2.791 2.407,2.791L2.406,2.793L2.404,2.795L2.399,2.802L2.381,2.826C2.366,2.846 2.346,2.873 2.321,2.908C2.272,2.978 2.203,3.079 2.121,3.207C1.958,3.463 1.744,3.831 1.529,4.29C1.103,5.204 0.667,6.502 0.667,8C0.667,9.499 1.103,10.797 1.529,11.711C1.744,12.17 1.958,12.538 2.121,12.794C2.203,12.922 2.272,13.023 2.321,13.093C2.346,13.128 2.366,13.155 2.381,13.175L2.399,13.198L2.404,13.205L2.406,13.208L2.407,13.209C2.407,13.209 2.407,13.21 2.934,12.8L2.407,13.21Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M4.14,10.809C4.366,11.1 4.785,11.153 5.076,10.926C5.365,10.701 5.418,10.284 5.194,9.993L5.192,9.99C5.189,9.986 5.183,9.978 5.175,9.967C5.16,9.945 5.135,9.909 5.104,9.86C5.042,9.762 4.956,9.616 4.87,9.433C4.697,9.061 4.533,8.559 4.533,8C4.533,7.442 4.697,6.94 4.87,6.568C4.956,6.384 5.042,6.238 5.104,6.14C5.135,6.092 5.16,6.055 5.175,6.033C5.183,6.022 5.189,6.014 5.192,6.01L5.194,6.007C5.418,5.717 5.365,5.299 5.076,5.074C4.785,4.848 4.366,4.9 4.14,5.191L4.666,5.6C4.14,5.191 4.14,5.191 4.14,5.191L4.139,5.192L4.137,5.194L4.134,5.199L4.123,5.214C4.114,5.226 4.102,5.241 4.088,5.261C4.061,5.3 4.023,5.355 3.979,5.424C3.891,5.562 3.776,5.759 3.662,6.004C3.436,6.489 3.2,7.187 3.2,8C3.2,8.813 3.436,9.511 3.662,9.996C3.776,10.241 3.891,10.438 3.979,10.576C4.023,10.645 4.061,10.7 4.088,10.739C4.102,10.759 4.114,10.775 4.123,10.786L4.134,10.801L4.137,10.806L4.139,10.808L4.14,10.809C4.14,10.809 4.14,10.809 4.666,10.4L4.14,10.809Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M8,8m-1.333,0a1.333,1.333 0,1 1,2.667 0a1.333,1.333 0,1 1,-2.667 0"
android:fillColor="#ffffff"/>
</vector>

View 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>

View File

@ -7,8 +7,7 @@
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"
@ -24,7 +23,7 @@
android:singleLine="true" android:singleLine="true"
android:text="@string/voice_broadcast_live" android:text="@string/voice_broadcast_live"
android:textColor="?colorOnError" android:textColor="?colorOnError"
app:drawableStartCompat="@drawable/ic_voice_broadcast_16" app:drawableStartCompat="@drawable/ic_attachment_voice_broadcast"
app:drawableTint="?colorOnError" 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 +53,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_height="wrap_content"
tools:text="@sample/users.json/data/displayName" />
</LinearLayout>
<LinearLayout
android:id="@+id/voiceBroadcastViewGroup"
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_mic"
android:gravity="center_vertical" tools:metadataValue="@sample/users.json/data/displayName" />
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/voiceBroadcastMetadata"
android:layout_width="16dp" android:layout_width="wrap_content"
android:layout_height="16dp" android:layout_height="wrap_content"
android:layout_marginEnd="5dp" app:metadataIcon="@drawable/ic_attachment_voice_broadcast"
android:contentDescription="@null" app:metadataValue="@string/attachment_type_voice_broadcast" />
android:src="@drawable/ic_voice_broadcast_16"
app:tint="?vctr_content_secondary" />
<TextView <im.vector.app.features.voicebroadcast.views.VoiceBroadcastMetadataView
android:id="@+id/voiceBroadcastText" android:id="@+id/listenersCountMetadata"
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" app:metadataIcon="@drawable/ic_member_small"
android:text="@string/attachment_type_voice_broadcast" /> app:metadataValue="@string/no_value_placeholder"
</LinearLayout> tools:metadataValue="5 listeners" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/headerBottomBarrier" android:id="@+id/headerBottomBarrier"
@ -116,7 +95,7 @@
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 <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/controllerButtonsFlow" android:id="@+id/controllerButtonsFlow"

View File

@ -7,8 +7,7 @@
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"
@ -24,7 +23,7 @@
android:singleLine="true" android:singleLine="true"
android:text="@string/voice_broadcast_live" android:text="@string/voice_broadcast_live"
android:textColor="?colorOnError" android:textColor="?colorOnError"
app:drawableStartCompat="@drawable/ic_voice_broadcast_16" app:drawableStartCompat="@drawable/ic_attachment_voice_broadcast"
app:drawableTint="?colorOnError" 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 +53,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 +88,7 @@
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 <androidx.constraintlayout.helper.widget.Flow
android:id="@+id/controllerButtonsFlow" android:id="@+id/controllerButtonsFlow"

View 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_attachment_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>