Toggle navigation
Toggle navigation
此项目
正在载入...
Sign in
xuning
/
livekitAndroidXuningTest
转到一个项目
Toggle navigation
项目
群组
代码片段
帮助
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
davidliu
2022-07-12 01:44:04 +0900
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
f6bc06ca6c919bd7a3d3ca2b221a6e4ca0398042
f6bc06ca
1 parent
2f349057
Revert "Revert "Update samples (#102)""
This reverts commit
facd5b6d
.
隐藏空白字符变更
内嵌
并排对比
正在显示
6 个修改的文件
包含
235 行增加
和
249 行删除
sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt
sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt
sample-app/src/main/java/io/livekit/android/sample/CallActivity.kt
sample-app/src/main/java/io/livekit/android/sample/ParticipantItem.kt
sample-app/src/main/res/layout/call_activity.xml
sample-app/src/main/res/layout/speaker_view.xml
sample-app-common/src/main/java/io/livekit/android/sample/CallViewModel.kt
查看文件 @
f6bc06c
...
...
@@ -30,22 +30,20 @@ class CallViewModel(
val token: String,
application: Application
) : AndroidViewModel(application) {
private val mutableRoom = MutableStateFlow<Room?>(null)
val room: MutableStateFlow<Room?> = mutableRoom
val participants = mutableRoom.flatMapLatest { room ->
if (room != null) {
room::remoteParticipants.flow
.map { remoteParticipants ->
listOf<Participant>(room.localParticipant) +
remoteParticipants
.keys
.sortedBy { it }
.mapNotNull { remoteParticipants[it] }
}
} else {
flowOf(emptyList())
val room = LiveKit.create(
appContext = application,
options = RoomOptions(adaptiveStream = true, dynacast = true),
)
val participants = room::remoteParticipants.flow
.map { remoteParticipants ->
listOf<Participant>(room.localParticipant) +
remoteParticipants
.keys
.sortedBy { it }
.mapNotNull { remoteParticipants[it] }
}
}
private val mutableError = MutableStateFlow<Throwable?>(null)
val error = mutableError.hide()
...
...
@@ -53,13 +51,7 @@ class CallViewModel(
private val mutablePrimarySpeaker = MutableStateFlow<Participant?>(null)
val primarySpeaker: StateFlow<Participant?> = mutablePrimarySpeaker
val activeSpeakers = mutableRoom.flatMapLatest { room ->
if (room != null) {
room::activeSpeakers.flow
} else {
flowOf(emptyList())
}
}
val activeSpeakers = room::activeSpeakers.flow
private var localScreencastTrack: LocalScreencastVideoTrack? = null
...
...
@@ -84,61 +76,60 @@ class CallViewModel(
val audioHandler = AudioSwitchHandler(application)
init {
viewModelScope.launch {
launch {
error.collect { Timber.e(it) }
}
try {
val room = LiveKit.connect(
application,
url,
token,
roomOptions = RoomOptions(adaptiveStream = true, dynacast = true),
overrides = LiveKitOverrides(audioHandler = audioHandler)
)
// Create and publish audio/video tracks
val localParticipant = room.localParticipant
localParticipant.setMicrophoneEnabled(true)
mutableMicEnabled.postValue(localParticipant.isMicrophoneEnabled())
localParticipant.setCameraEnabled(true)
mutableCameraEnabled.postValue(localParticipant.isCameraEnabled())
mutableRoom.value = room
handlePrimarySpeaker(emptyList(), emptyList(), room)
launch {
combine(participants, activeSpeakers) { participants, speakers -> participants to speakers }
.collect { (participantsList, speakers) ->
handlePrimarySpeaker(
participantsList,
speakers,
room
)
}
}
launch {
combine(participants, activeSpeakers) { participants, speakers -> participants to speakers }
.collect { (participantsList, speakers) ->
handlePrimarySpeaker(
participantsList,
speakers,
room
)
}
}
launch {
room.events.collect {
when (it) {
is RoomEvent.FailedToConnect -> mutableError.value = it.error
is RoomEvent.DataReceived -> {
val identity = it.participant.identity ?: ""
val message = it.data.toString(Charsets.UTF_8)
mutableDataReceived.emit("$identity: $message")
}
launch {
room.events.collect {
when (it) {
is RoomEvent.FailedToConnect -> mutableError.value = it.error
is RoomEvent.DataReceived -> {
val identity = it.participant.identity ?: ""
val message = it.data.toString(Charsets.UTF_8)
mutableDataReceived.emit("$identity: $message")
}
else -> {}
}
}
} catch (e: Throwable) {
mutableError.value = e
}
connectToRoom()
}
}
private fun handlePrimarySpeaker(participantsList: List<Participant>, speakers: List<Participant>, room: Room) {
private suspend fun connectToRoom() {
try {
room.connect(
url = url,
token = token,
)
// Create and publish audio/video tracks
val localParticipant = room.localParticipant
localParticipant.setMicrophoneEnabled(true)
mutableMicEnabled.postValue(localParticipant.isMicrophoneEnabled())
localParticipant.setCameraEnabled(true)
mutableCameraEnabled.postValue(localParticipant.isCameraEnabled())
handlePrimarySpeaker(emptyList(), emptyList(), room)
} catch (e: Throwable) {
mutableError.value = e
}
}
private fun handlePrimarySpeaker(participantsList: List<Participant>, speakers: List<Participant>, room: Room?) {
var speaker = mutablePrimarySpeaker.value
...
...
@@ -159,7 +150,7 @@ class CallViewModel(
// Default to another person in room, or local participant.
speaker = participantsList.filterIsInstance<RemoteParticipant>()
.firstOrNull()
?: room.localParticipant
?: room
?
.localParticipant
}
if (speakers.isNotEmpty() && !speakers.contains(speaker)) {
...
...
@@ -176,7 +167,7 @@ class CallViewModel(
}
fun startScreenCapture(mediaProjectionPermissionResultData: Intent) {
val localParticipant = room.
value?.localParticipant ?: return
val localParticipant = room.
localParticipant
viewModelScope.launch {
val screencastTrack =
localParticipant.createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
...
...
@@ -197,7 +188,7 @@ class CallViewModel(
viewModelScope.launch {
localScreencastTrack?.let { localScreencastVideoTrack ->
localScreencastVideoTrack.stop()
room.
value?.localParticipant?
.unpublishTrack(localScreencastVideoTrack)
room.
localParticipant
.unpublishTrack(localScreencastVideoTrack)
mutableScreencastEnabled.postValue(localScreencastTrack?.enabled ?: false)
}
}
...
...
@@ -205,39 +196,35 @@ class CallViewModel(
override fun onCleared() {
super.onCleared()
mutableRoom.value?
.disconnect()
room
.disconnect()
}
fun setMicEnabled(enabled: Boolean) {
viewModelScope.launch {
val localParticipant = room.value?.localParticipant ?: return@launch
localParticipant.setMicrophoneEnabled(enabled)
room.localParticipant.setMicrophoneEnabled(enabled)
mutableMicEnabled.postValue(enabled)
}
}
fun setCameraEnabled(enabled: Boolean) {
viewModelScope.launch {
val localParticipant = room.value?.localParticipant ?: return@launch
localParticipant.setCameraEnabled(enabled)
room.localParticipant.setCameraEnabled(enabled)
mutableCameraEnabled.postValue(enabled)
}
}
fun flipCamera() {
room.value?.localParticipant?.let { participant ->
val videoTrack = participant.getTrackPublication(Track.Source.CAMERA)
?.track as? LocalVideoTrack
?: return@let
val newOptions = when (videoTrack.options.position) {
CameraPosition.FRONT -> LocalVideoTrackOptions(position = CameraPosition.BACK)
CameraPosition.BACK -> LocalVideoTrackOptions(position = CameraPosition.FRONT)
else -> LocalVideoTrackOptions()
}
videoTrack.restartTrack(newOptions)
val videoTrack = room.localParticipant.getTrackPublication(Track.Source.CAMERA)
?.track as? LocalVideoTrack
?: return
val newOptions = when (videoTrack.options.position) {
CameraPosition.FRONT -> LocalVideoTrackOptions(position = CameraPosition.BACK)
CameraPosition.BACK -> LocalVideoTrackOptions(position = CameraPosition.FRONT)
else -> LocalVideoTrackOptions()
}
videoTrack.restartTrack(newOptions)
}
fun dismissError() {
...
...
@@ -246,17 +233,17 @@ class CallViewModel(
fun sendData(message: String) {
viewModelScope.launch {
room.
value?.localParticipant?
.publishData(message.toByteArray(Charsets.UTF_8))
room.
localParticipant
.publishData(message.toByteArray(Charsets.UTF_8))
}
}
fun toggleSubscriptionPermissions() {
mutablePermissionAllowed.value = !mutablePermissionAllowed.value
room.
value?.localParticipant?
.setTrackSubscriptionPermissions(mutablePermissionAllowed.value)
room.
localParticipant
.setTrackSubscriptionPermissions(mutablePermissionAllowed.value)
}
fun simulateMigration() {
room.
value?.
sendSimulateScenario(
room.sendSimulateScenario(
LivekitRtc.SimulateScenario.newBuilder()
.setMigration(true)
.build()
...
...
@@ -265,21 +252,14 @@ class CallViewModel(
fun reconnect() {
Timber.e { "Reconnecting." }
val room = mutableRoom.value ?: return
mutableRoom.value = null
mutablePrimarySpeaker.value = null
room.disconnect()
viewModelScope.launch {
room.connect(
url,
token
)
mutableRoom.value = room
connectToRoom()
}
}
}
private fun <T> LiveData<T>.hide(): LiveData<T> = this
private fun <T> MutableStateFlow<T>.hide(): StateFlow<T> = this
private fun <T> Flow<T>.hide(): Flow<T> = this
\ No newline at end of file
...
...
sample-app-compose/src/main/java/io/livekit/android/composesample/CallActivity.kt
查看文件 @
f6bc06c
...
...
@@ -62,7 +62,7 @@ class CallActivity : AppCompatActivity() {
// Setup compose view.
setContent {
val room
by viewModel.room.collectAsState()
val room
= viewModel.room
val participants by viewModel.participants.collectAsState(initial = emptyList())
val primarySpeaker by viewModel.primarySpeaker.collectAsState()
val activeSpeakers by viewModel.activeSpeakers.collectAsState(initial = emptyList())
...
...
sample-app/src/main/java/io/livekit/android/sample/CallActivity.kt
查看文件 @
f6bc06c
...
...
@@ -4,7 +4,6 @@ import android.app.Activity
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.widget.EditText
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
...
...
@@ -13,13 +12,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.xwray.groupie.GroupieAdapter
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.VideoTrack
import io.livekit.android.sample.databinding.CallActivityBinding
import io.livekit.android.sample.dialog.showDebugMenuDialog
import io.livekit.android.sample.dialog.showSelectAudioDeviceDialog
import io.livekit.android.util.flow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.collectLatest
import kotlinx.parcelize.Parcelize
class CallActivity : AppCompatActivity() {
...
...
@@ -50,87 +46,32 @@ class CallActivity : AppCompatActivity() {
setContentView(binding.root)
// Audience row setup
binding.audienceRow.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
val adapter = GroupieAdapter()
val audienceAdapter = GroupieAdapter()
binding.audienceRow.apply {
this.adapter = adapter
layoutManager = LinearLayoutManager(this@CallActivity, LinearLayoutManager.HORIZONTAL, false)
adapter = audienceAdapter
}
lifecycleScope.launchWhenCreated {
viewModel.room
.combine(viewModel.participants) { room, participants -> room to participants }
.collect { (room, participants) ->
if (room != null) {
val items = participants.map { participant -> ParticipantItem(room, participant) }
adapter.update(items)
}
viewModel.participants
.collect { participants ->
val items = participants.map { participant -> ParticipantItem(viewModel.room, participant) }
audienceAdapter.update(items)
}
}
// speaker view setup
val speakerAdapter = GroupieAdapter()
binding.speakerView.apply {
layoutManager = LinearLayoutManager(this@CallActivity, LinearLayoutManager.HORIZONTAL, false)
adapter = speakerAdapter
}
lifecycleScope.launchWhenCreated {
viewModel.room.filterNotNull().take(1)
.transform { room ->
// Initialize video renderer
room.initVideoRenderer(binding.speakerVideoView)
// Observe primary speaker changes
emitAll(viewModel.primarySpeaker)
}.flatMapLatest { primarySpeaker ->
if (primarySpeaker != null) {
flowOf(primarySpeaker)
} else {
emptyFlow()
}
}.collect { participant ->
// Update new primary speaker identity
binding.identityText.text = participant.identity
// observe videoTracks changes.
val videoTrackFlow = participant::videoTracks.flow
.map { participant to it }
.flatMapLatest { (participant, videoTracks) ->
// Prioritize any screenshare streams.
val trackPublication = participant.getTrackPublication(Track.Source.SCREEN_SHARE)
?: participant.getTrackPublication(Track.Source.CAMERA)
?: videoTracks.firstOrNull()?.first
?: return@flatMapLatest emptyFlow()
trackPublication::track.flow
}
// observe audioTracks changes.
val mutedFlow = participant::audioTracks.flow
.flatMapLatest { tracks ->
val audioTrack = tracks.firstOrNull()?.first
if (audioTrack != null) {
audioTrack::muted.flow
} else {
flowOf(true)
}
}
combine(videoTrackFlow, mutedFlow) { videoTrack, muted ->
videoTrack to muted
}.collect { (videoTrack, muted) ->
// Cleanup old video track
val oldVideoTrack = binding.speakerVideoView.tag as? VideoTrack
oldVideoTrack?.removeRenderer(binding.speakerVideoView)
// Bind new video track to video view.
if (videoTrack is VideoTrack) {
videoTrack.addRenderer(binding.speakerVideoView)
binding.speakerVideoView.visibility = View.VISIBLE
} else {
binding.speakerVideoView.visibility = View.INVISIBLE
}
binding.speakerVideoView.tag = videoTrack
binding.muteIndicator.visibility = if (muted) View.VISIBLE else View.INVISIBLE
}
}
viewModel.primarySpeaker.collectLatest { speaker ->
val items = listOfNotNull(speaker)
.map { participant -> ParticipantItem(viewModel.room, participant, speakerView = true) }
speakerAdapter.update(items)
}
}
// Controls setup
...
...
@@ -224,10 +165,9 @@ class CallActivity : AppCompatActivity() {
}
override fun onDestroy() {
binding.audienceRow.adapter = null
binding.speakerView.adapter = null
super.onDestroy()
// Release video views
binding.speakerVideoView.release()
}
companion object {
...
...
sample-app/src/main/java/io/livekit/android/sample/ParticipantItem.kt
查看文件 @
f6bc06c
...
...
@@ -7,20 +7,18 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder
import io.livekit.android.room.Room
import io.livekit.android.room.participant.ConnectionQuality
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.ParticipantListener
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.RemoteTrackPublication
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.VideoTrack
import io.livekit.android.sample.databinding.ParticipantItemBinding
import io.livekit.android.util.flow
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.*
@OptIn(ExperimentalCoroutinesApi::class)
class ParticipantItem(
private val room: Room,
private val participant: Participant
private val participant: Participant,
private val speakerView: Boolean = false,
) : BindableItem<ParticipantItemBinding>() {
private var boundVideoTrack: VideoTrack? = null
...
...
@@ -67,25 +65,44 @@ class ParticipantItem(
if (quality == ConnectionQuality.POOR) View.VISIBLE else View.INVISIBLE
}
}
participant.listener = object : ParticipantListener {
override fun onTrackSubscribed(
track: Track,
publication: RemoteTrackPublication,
participant: RemoteParticipant
) {
if (track !is VideoTrack) return
if (publication.source == Track.Source.CAMERA) {
setupVideoIfNeeded(track, viewBinding)
}
}
override fun onTrackUnpublished(
publication: RemoteTrackPublication,
participant: RemoteParticipant
) {
super.onTrackUnpublished(publication, participant)
Timber.e { "Track unpublished" }
// observe videoTracks changes.
val videoTrackPubFlow = participant::videoTracks.flow
.map { participant to it }
.flatMapLatest { (participant, videoTracks) ->
// Prioritize any screenshare streams.
val trackPublication = participant.getTrackPublication(Track.Source.SCREEN_SHARE)
?: participant.getTrackPublication(Track.Source.CAMERA)
?: videoTracks.firstOrNull()?.first
flowOf(trackPublication)
}
coroutineScope?.launch {
videoTrackPubFlow
.flatMapLatest { pub ->
if (pub != null) {
pub::track.flow
} else {
flowOf(null)
}
}
.collectLatest { videoTrack ->
setupVideoIfNeeded(videoTrack as? VideoTrack, viewBinding)
}
}
coroutineScope?.launch {
videoTrackPubFlow
.flatMapLatest { pub ->
if (pub != null) {
pub::muted.flow
} else {
flowOf(true)
}
}
.collectLatest { muted ->
viewBinding.renderer.visibleOrInvisible(!muted)
}
}
val existingTrack = getVideoTrack()
if (existingTrack != null) {
...
...
@@ -97,14 +114,14 @@ class ParticipantItem(
return participant.getTrackPublication(Track.Source.CAMERA)?.track as? VideoTrack
}
internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) {
if (boundVideoTrack != null) {
private fun setupVideoIfNeeded(videoTrack: VideoTrack?, viewBinding: ParticipantItemBinding) {
if (boundVideoTrack == videoTrack) {
return
}
boundVideoTrack?.removeRenderer(viewBinding.renderer)
boundVideoTrack = videoTrack
Timber.v { "adding renderer to $videoTrack" }
videoTrack.addRenderer(viewBinding.renderer)
videoTrack
?
.addRenderer(viewBinding.renderer)
}
override fun unbind(viewHolder: GroupieViewHolder<ParticipantItemBinding>) {
...
...
@@ -115,5 +132,25 @@ class ParticipantItem(
boundVideoTrack = null
}
override fun getLayout(): Int = R.layout.participant_item
override fun getLayout(): Int =
if (speakerView)
R.layout.speaker_view
else
R.layout.participant_item
}
private fun View.visibleOrGone(visible: Boolean) {
visibility = if (visible) {
View.VISIBLE
} else {
View.GONE
}
}
private fun View.visibleOrInvisible(visible: Boolean) {
visibility = if (visible) {
View.VISIBLE
} else {
View.INVISIBLE
}
}
\ No newline at end of file
...
...
sample-app/src/main/res/layout/call_activity.xml
查看文件 @
f6bc06c
...
...
@@ -4,7 +4,7 @@
android:layout_height=
"match_parent"
android:keepScreenOn=
"true"
>
<
FrameLayout
<
androidx.recyclerview.widget.RecyclerView
android:id=
"@+id/speaker_view"
android:layout_width=
"0dp"
android:layout_height=
"0dp"
...
...
@@ -14,49 +14,7 @@
app:layout_constraintStart_toStartOf=
"parent"
app:layout_constraintTop_toTopOf=
"parent"
>
<ImageView
android:layout_width=
"120dp"
android:layout_height=
"120dp"
android:layout_gravity=
"center"
android:src=
"@drawable/outline_videocam_off_24"
app:tint=
"@color/no_video_participant"
/>
<io.livekit.android.renderer.TextureViewRenderer
android:id=
"@+id/speaker_video_view"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
/>
</FrameLayout>
<FrameLayout
android:id=
"@+id/identity_bar"
android:layout_width=
"0dp"
android:layout_height=
"30dp"
android:background=
"#80000000"
app:layout_constraintBottom_toBottomOf=
"@id/speaker_view"
app:layout_constraintEnd_toEndOf=
"parent"
app:layout_constraintStart_toStartOf=
"parent"
/>
<ImageView
android:id=
"@+id/mute_indicator"
android:layout_width=
"24dp"
android:layout_height=
"24dp"
android:layout_marginEnd=
"@dimen/identity_bar_padding"
android:src=
"@drawable/outline_mic_off_24"
app:layout_constraintBottom_toBottomOf=
"@id/identity_bar"
app:layout_constraintEnd_toEndOf=
"@id/identity_bar"
app:layout_constraintTop_toTopOf=
"@id/identity_bar"
app:tint=
"#BB0000"
/>
<TextView
android:id=
"@+id/identity_text"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_marginStart=
"@dimen/identity_bar_padding"
android:ellipsize=
"end"
app:layout_constraintBottom_toBottomOf=
"@id/identity_bar"
app:layout_constraintEnd_toStartOf=
"@id/mute_indicator"
app:layout_constraintStart_toStartOf=
"@id/identity_bar"
app:layout_constraintTop_toTopOf=
"@id/identity_bar"
/>
</androidx.recyclerview.widget.RecyclerView>
<androidx.recyclerview.widget.RecyclerView
android:id=
"@+id/audience_row"
...
...
sample-app/src/main/res/layout/speaker_view.xml
0 → 100644
查看文件 @
f6bc06c
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android=
"http://schemas.android.com/apk/res/android"
xmlns:app=
"http://schemas.android.com/apk/res-auto"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:background=
"@color/no_video_background"
>
<FrameLayout
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
android:background=
"@color/no_video_background"
app:layout_constraintStart_toStartOf=
"parent"
app:layout_constraintTop_toTopOf=
"parent"
>
<ImageView
android:layout_width=
"60dp"
android:layout_height=
"60dp"
android:layout_gravity=
"center"
android:src=
"@drawable/outline_videocam_off_24"
app:tint=
"@color/no_video_participant"
/>
<io.livekit.android.renderer.TextureViewRenderer
android:id=
"@+id/renderer"
android:layout_width=
"match_parent"
android:layout_height=
"match_parent"
/>
<ImageView
android:id=
"@+id/connection_quality"
android:layout_width=
"24dp"
android:layout_height=
"24dp"
android:layout_gravity=
"top|end"
android:layout_marginTop=
"@dimen/identity_bar_padding"
android:layout_marginEnd=
"@dimen/identity_bar_padding"
android:alpha=
"0.5"
android:src=
"@drawable/wifi_strength_1"
android:visibility=
"invisible"
app:tint=
"#FF0000"
/>
</FrameLayout>
<FrameLayout
android:id=
"@+id/identity_bar"
android:layout_width=
"0dp"
android:layout_height=
"30dp"
android:background=
"#80000000"
app:layout_constraintBottom_toBottomOf=
"parent"
app:layout_constraintEnd_toEndOf=
"parent"
app:layout_constraintStart_toStartOf=
"parent"
/>
<ImageView
android:id=
"@+id/mute_indicator"
android:layout_width=
"24dp"
android:layout_height=
"24dp"
android:layout_marginEnd=
"@dimen/identity_bar_padding"
android:src=
"@drawable/outline_mic_off_24"
android:visibility=
"gone"
app:layout_constraintBottom_toBottomOf=
"@id/identity_bar"
app:layout_constraintEnd_toEndOf=
"@id/identity_bar"
app:layout_constraintTop_toTopOf=
"@id/identity_bar"
app:tint=
"#BB0000"
/>
<TextView
android:id=
"@+id/identity_text"
android:layout_width=
"0dp"
android:layout_height=
"wrap_content"
android:layout_marginStart=
"@dimen/identity_bar_padding"
android:ellipsize=
"end"
app:layout_constraintBottom_toBottomOf=
"@id/identity_bar"
app:layout_constraintEnd_toStartOf=
"@id/mute_indicator"
app:layout_constraintStart_toStartOf=
"@id/identity_bar"
app:layout_constraintTop_toTopOf=
"@id/identity_bar"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
...
...
请
注册
或
登录
后发表评论