For #1114: Show playing tab
parent
215e66fb08
commit
999d3cb963
|
@ -6,13 +6,16 @@ package org.mozilla.fenix.ext
|
|||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.feature.media.state.MediaState
|
||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||
|
||||
fun Session.toTab(context: Context, selected: Boolean? = null): Tab {
|
||||
fun Session.toTab(context: Context, selected: Boolean? = null, mediaState: MediaState? = null): Tab {
|
||||
return Tab(
|
||||
this.id,
|
||||
this.url,
|
||||
this.url.urlToTrimmedHost(context),
|
||||
this.title,
|
||||
selected)
|
||||
selected,
|
||||
mediaState
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,6 +48,11 @@ import mozilla.components.concept.sync.AccountObserver
|
|||
import mozilla.components.concept.sync.AuthType
|
||||
import mozilla.components.concept.sync.OAuthAccount
|
||||
import mozilla.components.concept.sync.Profile
|
||||
import mozilla.components.feature.media.ext.getSession
|
||||
import mozilla.components.feature.media.ext.pauseIfPlaying
|
||||
import mozilla.components.feature.media.ext.playIfPaused
|
||||
import mozilla.components.feature.media.state.MediaState
|
||||
import mozilla.components.feature.media.state.MediaStateMachine
|
||||
import mozilla.components.feature.tab.collections.TabCollection
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.BOTTOM
|
||||
import org.jetbrains.anko.constraint.layout.ConstraintSetBuilder.Side.END
|
||||
|
@ -144,6 +149,7 @@ class HomeFragment : Fragment(), AccountObserver {
|
|||
val sessionObserver = BrowserSessionsObserver(sessionManager, singleSessionObserver) {
|
||||
emitSessionChanges()
|
||||
}
|
||||
|
||||
lifecycle.addObserver(sessionObserver)
|
||||
|
||||
if (!onboarding.userHasBeenOnboarded()) {
|
||||
|
@ -372,6 +378,12 @@ class HomeFragment : Fragment(), AccountObserver {
|
|||
share(session.url)
|
||||
}
|
||||
}
|
||||
is TabAction.PauseMedia -> {
|
||||
MediaStateMachine.state.pauseIfPlaying()
|
||||
}
|
||||
is TabAction.PlayMedia -> {
|
||||
MediaStateMachine.state.playIfPaused()
|
||||
}
|
||||
is TabAction.CloseAll -> {
|
||||
if (pendingSessionDeletion?.deletionJob == null) {
|
||||
removeAllTabsWithUndo(
|
||||
|
@ -926,7 +938,17 @@ class HomeFragment : Fragment(), AccountObserver {
|
|||
|
||||
private fun List<Session>.toTabs(): List<Tab> {
|
||||
val selected = sessionManager.selectedSession
|
||||
return this.map { it.toTab(requireContext(), it == selected) }
|
||||
val mediaStateSession = MediaStateMachine.state.getSession()
|
||||
|
||||
return this.map {
|
||||
val mediaState = if (mediaStateSession?.id == it.id) {
|
||||
MediaStateMachine.state
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
it.toTab(requireContext(), it == selected, mediaState)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -935,8 +957,6 @@ class HomeFragment : Fragment(), AccountObserver {
|
|||
private const val ANIM_ON_SCREEN_DELAY = 200L
|
||||
private const val FADE_ANIM_DURATION = 150L
|
||||
private const val ANIM_SNACKBAR_DELAY = 100L
|
||||
private const val ACCESSIBILITY_FOCUS_DELAY = 2000L
|
||||
private const val TELEMETRY_HOME_IDENITIFIER = "home"
|
||||
private const val SHARED_TRANSITION_MS = 200L
|
||||
private const val TAB_ITEM_TRANSITION_NAME = "tab_item"
|
||||
private const val CFR_WIDTH_DIVIDER = 1.7
|
||||
|
@ -966,6 +986,7 @@ private class BrowserSessionsObserver(
|
|||
*/
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_START)
|
||||
fun onStart() {
|
||||
MediaStateMachine.register(managerObserver)
|
||||
manager.register(managerObserver)
|
||||
subscribeToAll()
|
||||
}
|
||||
|
@ -975,6 +996,7 @@ private class BrowserSessionsObserver(
|
|||
*/
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
|
||||
fun onStop() {
|
||||
MediaStateMachine.unregister(managerObserver)
|
||||
manager.unregister(managerObserver)
|
||||
unsubscribeFromAll()
|
||||
}
|
||||
|
@ -995,7 +1017,11 @@ private class BrowserSessionsObserver(
|
|||
session.unregister(observer)
|
||||
}
|
||||
|
||||
private val managerObserver = object : SessionManager.Observer {
|
||||
private val managerObserver = object : SessionManager.Observer, MediaStateMachine.Observer {
|
||||
override fun onStateChanged(state: MediaState) {
|
||||
onChanged()
|
||||
}
|
||||
|
||||
override fun onSessionAdded(session: Session) {
|
||||
subscribeTo(session)
|
||||
onChanged()
|
||||
|
|
|
@ -6,6 +6,7 @@ package org.mozilla.fenix.home.sessioncontrol
|
|||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.LayoutRes
|
||||
|
@ -14,6 +15,8 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.Observer
|
||||
import kotlinx.android.synthetic.main.tab_list_row.*
|
||||
import mozilla.components.feature.media.state.MediaState
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
|
||||
import org.mozilla.fenix.home.sessioncontrol.viewholders.NoContentMessageViewHolder
|
||||
|
@ -37,7 +40,28 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
|
|||
data class TabHeader(val isPrivate: Boolean, val hasTabs: Boolean) : AdapterItem(TabHeaderViewHolder.LAYOUT_ID)
|
||||
data class TabItem(val tab: Tab) : AdapterItem(TabViewHolder.LAYOUT_ID) {
|
||||
override fun sameAs(other: AdapterItem) = other is TabItem && tab.sessionId == other.tab.sessionId
|
||||
|
||||
// Tell the adapter exactly what values have changed so it only has to draw those
|
||||
override fun getChangePayload(newItem: AdapterItem): Any? {
|
||||
(newItem as TabItem).let {
|
||||
val shouldUpdateUrl = newItem.tab.url != this.tab.url
|
||||
val shouldUpdateHostname = newItem.tab.hostname != this.tab.hostname
|
||||
val shouldUpdateTitle = newItem.tab.title != this.tab.title
|
||||
val shouldUpdateSelected = newItem.tab.selected != this.tab.selected
|
||||
val shouldUpdateMediaState = newItem.tab.mediaState != this.tab.mediaState
|
||||
|
||||
return AdapterItemDiffCallback.TabChangePayload(
|
||||
tab = newItem.tab,
|
||||
shouldUpdateUrl = shouldUpdateUrl,
|
||||
shouldUpdateHostname = shouldUpdateHostname,
|
||||
shouldUpdateTitle = shouldUpdateTitle,
|
||||
shouldUpdateSelected = shouldUpdateSelected,
|
||||
shouldUpdateMediaState = shouldUpdateMediaState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SaveTabGroup : AdapterItem(SaveTabGroupViewHolder.LAYOUT_ID)
|
||||
|
||||
object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID)
|
||||
|
@ -85,6 +109,11 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
|
|||
* True if this item represents the same value as other. Used by [AdapterItemDiffCallback].
|
||||
*/
|
||||
open fun sameAs(other: AdapterItem) = this::class == other::class
|
||||
|
||||
/**
|
||||
* Returns a payload if there's been a change, or null if not
|
||||
*/
|
||||
open fun getChangePayload(newItem: AdapterItem): Any? = null
|
||||
}
|
||||
|
||||
class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
|
||||
|
@ -92,6 +121,19 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
|
|||
|
||||
@Suppress("DiffUtilEquals")
|
||||
override fun areContentsTheSame(oldItem: AdapterItem, newItem: AdapterItem) = oldItem == newItem
|
||||
|
||||
override fun getChangePayload(oldItem: AdapterItem, newItem: AdapterItem): Any? {
|
||||
return oldItem.getChangePayload(newItem) ?: return super.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
|
||||
data class TabChangePayload(
|
||||
val tab: Tab,
|
||||
val shouldUpdateUrl: Boolean,
|
||||
val shouldUpdateHostname: Boolean,
|
||||
val shouldUpdateTitle: Boolean,
|
||||
val shouldUpdateSelected: Boolean,
|
||||
val shouldUpdateMediaState: Boolean
|
||||
)
|
||||
}
|
||||
|
||||
class SessionControlAdapter(
|
||||
|
@ -133,9 +175,9 @@ class SessionControlAdapter(
|
|||
val tabHeader = item as AdapterItem.TabHeader
|
||||
holder.bind(tabHeader.isPrivate, tabHeader.hasTabs)
|
||||
}
|
||||
is TabViewHolder -> holder.bindSession(
|
||||
(item as AdapterItem.TabItem).tab
|
||||
)
|
||||
is TabViewHolder -> {
|
||||
holder.bindSession((item as AdapterItem.TabItem).tab)
|
||||
}
|
||||
is NoContentMessageViewHolder -> {
|
||||
val (icon, header, description) = item as AdapterItem.NoContentMessage
|
||||
holder.bind(icon, header, description)
|
||||
|
@ -152,13 +194,36 @@ class SessionControlAdapter(
|
|||
(item as AdapterItem.OnboardingSectionHeader).labelBuilder
|
||||
)
|
||||
is OnboardingManualSignInViewHolder -> holder.bind()
|
||||
is OnboardingAutomaticSignInViewHolder -> holder.bind(
|
||||
(
|
||||
(
|
||||
item as AdapterItem.OnboardingAutomaticSignIn
|
||||
).state as OnboardingState.SignedOutCanAutoSignIn
|
||||
).withAccount
|
||||
is OnboardingAutomaticSignInViewHolder -> holder.bind((
|
||||
(item as AdapterItem.OnboardingAutomaticSignIn).state
|
||||
as OnboardingState.SignedOutCanAutoSignIn).withAccount
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(
|
||||
holder: RecyclerView.ViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
return
|
||||
}
|
||||
|
||||
(payloads[0] as AdapterItemDiffCallback.TabChangePayload).let {
|
||||
(holder as TabViewHolder).updateTab(it.tab)
|
||||
|
||||
// Always set the visibility to GONE to avoid the play button sticking around from previous draws
|
||||
holder.play_pause_button.visibility = View.GONE
|
||||
|
||||
if (it.shouldUpdateHostname) { holder.updateHostname(it.tab.hostname) }
|
||||
if (it.shouldUpdateTitle) { holder.updateTitle(it.tab.title) }
|
||||
if (it.shouldUpdateUrl) { holder.updateFavIcon(it.tab.url) }
|
||||
if (it.shouldUpdateSelected) { holder.updateSelected(it.tab.selected ?: false) }
|
||||
if (it.shouldUpdateMediaState) {
|
||||
holder.updatePlayPauseButton(it.tab.mediaState ?: MediaState.None)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,12 @@
|
|||
package org.mozilla.fenix.home.sessioncontrol
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.reactivex.Observer
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.feature.media.state.MediaState
|
||||
import mozilla.components.service.fxa.sharing.ShareableAccount
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
@ -46,14 +45,14 @@ class SessionControlComponent(
|
|||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Tab(
|
||||
val sessionId: String,
|
||||
val url: String,
|
||||
val hostname: String,
|
||||
val title: String,
|
||||
val selected: Boolean? = null
|
||||
) : Parcelable
|
||||
val selected: Boolean? = null,
|
||||
var mediaState: MediaState? = null
|
||||
)
|
||||
|
||||
fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> {
|
||||
val sessionBundle = mutableListOf<Session>()
|
||||
|
@ -62,7 +61,6 @@ fun List<Tab>.toSessionBundle(context: Context): MutableList<Session> {
|
|||
sessionBundle.add(session)
|
||||
}
|
||||
}
|
||||
|
||||
return sessionBundle
|
||||
}
|
||||
|
||||
|
@ -108,6 +106,8 @@ sealed class TabAction : Action {
|
|||
data class Select(val tabView: View, val sessionId: String) : TabAction()
|
||||
data class Close(val sessionId: String) : TabAction()
|
||||
data class Share(val sessionId: String) : TabAction()
|
||||
data class PauseMedia(val sessionId: String) : TabAction()
|
||||
data class PlayMedia(val sessionId: String) : TabAction()
|
||||
object PrivateBrowsingLearnMore : TabAction()
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,12 @@ import kotlinx.android.extensions.LayoutContainer
|
|||
import kotlinx.android.synthetic.main.tab_list_row.*
|
||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||
import mozilla.components.feature.media.state.MediaState
|
||||
import mozilla.components.support.ktx.android.util.dpToFloat
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.increaseTapArea
|
||||
import org.mozilla.fenix.ext.loadIntoView
|
||||
import org.mozilla.fenix.home.sessioncontrol.SessionControlAction
|
||||
import org.mozilla.fenix.home.sessioncontrol.Tab
|
||||
|
@ -31,7 +33,7 @@ class TabViewHolder(
|
|||
) :
|
||||
RecyclerView.ViewHolder(view), LayoutContainer {
|
||||
|
||||
var tab: Tab? = null
|
||||
internal var tab: Tab? = null
|
||||
private var tabMenu: TabItemMenu
|
||||
|
||||
init {
|
||||
|
@ -52,9 +54,21 @@ class TabViewHolder(
|
|||
true
|
||||
}
|
||||
|
||||
close_tab_button?.run {
|
||||
setOnClickListener {
|
||||
actionEmitter.onNext(TabAction.Close(tab?.sessionId!!))
|
||||
close_tab_button.setOnClickListener {
|
||||
actionEmitter.onNext(TabAction.Close(tab?.sessionId!!))
|
||||
}
|
||||
|
||||
play_pause_button.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS)
|
||||
|
||||
play_pause_button.setOnClickListener {
|
||||
when (tab?.mediaState) {
|
||||
is MediaState.Playing -> {
|
||||
actionEmitter.onNext(TabAction.PauseMedia(tab?.sessionId!!))
|
||||
}
|
||||
|
||||
is MediaState.Paused -> {
|
||||
actionEmitter.onNext(TabAction.PlayMedia(tab?.sessionId!!))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,27 +86,59 @@ class TabViewHolder(
|
|||
}
|
||||
}
|
||||
|
||||
fun bindSession(tab: Tab) {
|
||||
this.tab = tab
|
||||
updateTabUI(tab)
|
||||
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}"
|
||||
internal fun bindSession(tab: Tab) {
|
||||
updateTab(tab)
|
||||
updateTitle(tab.title)
|
||||
updateHostname(tab.hostname)
|
||||
updateFavIcon(tab.url)
|
||||
updateSelected(tab.selected ?: false)
|
||||
updatePlayPauseButton(tab.mediaState ?: MediaState.None)
|
||||
item_tab.transitionName = "$TAB_ITEM_TRANSITION_NAME${tab.sessionId}"
|
||||
}
|
||||
|
||||
private fun updateTabUI(tab: Tab) {
|
||||
hostname.text = tab.hostname
|
||||
tab_title.text = tab.title
|
||||
favicon_image.context.components.core.icons.loadIntoView(favicon_image, tab.url)
|
||||
internal fun updatePlayPauseButton(mediaState: MediaState) {
|
||||
with(play_pause_button) {
|
||||
visibility = if (mediaState is MediaState.Playing || mediaState is MediaState.Paused) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
|
||||
if (mediaState is MediaState.Playing) {
|
||||
play_pause_button.contentDescription =
|
||||
context.getString(R.string.mozac_feature_media_notification_action_pause)
|
||||
setImageDrawable(context.getDrawable(R.drawable.pause_with_background))
|
||||
} else {
|
||||
play_pause_button.contentDescription =
|
||||
context.getString(R.string.mozac_feature_media_notification_action_play)
|
||||
setImageDrawable(context.getDrawable(R.drawable.play_with_background))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSelected(selected: Boolean) {
|
||||
internal fun updateTab(tab: Tab) {
|
||||
this.tab = tab
|
||||
}
|
||||
internal fun updateTitle(text: String) {
|
||||
tab_title.text = text
|
||||
}
|
||||
|
||||
internal fun updateHostname(text: String) {
|
||||
hostname.text = text
|
||||
}
|
||||
|
||||
internal fun updateFavIcon(url: String) {
|
||||
favicon_image.context.components.core.icons.loadIntoView(favicon_image, url)
|
||||
}
|
||||
|
||||
internal fun updateSelected(selected: Boolean) {
|
||||
selected_border.visibility = if (selected) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAB_ITEM_TRANSITION_NAME = "tab_item"
|
||||
private const val PLAY_PAUSE_BUTTON_EXTRA_DPS = 24
|
||||
const val LAYOUT_ID = R.layout.tab_list_row
|
||||
const val buttonIncreaseDps = 12
|
||||
const val favIconBorderRadiusInPx = 4
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="24dp"
|
||||
android:height="24dp" />
|
||||
<solid android:color="?above" />
|
||||
<stroke android:color="?above"
|
||||
android:width="2dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?accent"
|
||||
android:pathData="M12,2a10,10 0,1 0,10 10A10,10 0,0 0,12 2zM10.5,16.125a0.375,0.375 0,0 1,-0.375 0.375h-2.25A0.375,0.375 0,0 1,7.5 16.125v-8.25A0.375,0.375 0,0 1,7.875 7.5h2.25A0.375,0.375 0,0 1,10.5 7.875zM16.5,16.125a0.375,0.375 0,0 1,-0.375 0.375h-2.25a0.375,0.375 0,0 1,-0.375 -0.375v-8.25A0.375,0.375 0,0 1,13.875 7.5h2.25A0.375,0.375 0,0 1,16.5 7.875z"/>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<size
|
||||
android:width="24dp"
|
||||
android:height="24dp" />
|
||||
<solid android:color="?above" />
|
||||
<stroke android:color="?above"
|
||||
android:width="2dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<vector
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?accent"
|
||||
android:pathData="M12,22a10,10 0,1 1,10 -10,10 10,0 0,1 -10,10zM10,16.363l6,-3.5a1,1 0,0 0,0 -1.732l-6,-3.5A1,1 0,0 0,8.5 8.5v7a1,1 0,0 0,1.5 0.863z"/>
|
||||
</vector>
|
||||
</item>
|
||||
</layer-list>
|
|
@ -35,6 +35,21 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/play_pause_button"
|
||||
android:contentDescription="@string/mozac_feature_media_notification_action_pause"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:elevation="10dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:src="@drawable/pause_with_background"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintCircleAngle="45"
|
||||
app:layout_constraintCircleRadius="30dp"
|
||||
app:layout_constraintCircle="@id/favicon_image"
|
||||
app:layout_constraintEnd_toEndOf="@id/favicon_image"
|
||||
app:layout_constraintTop_toTopOf="@id/favicon_image" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hostname"
|
||||
android:layout_width="0dp"
|
||||
|
@ -75,13 +90,15 @@
|
|||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/selected_border"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/session_border"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:id="@+id/selected_border"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/session_border"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
Loading…
Reference in New Issue