Integration into location message item

This commit is contained in:
Maxime NATUREL 2022-04-05 16:11:01 +02:00
parent 3acc139307
commit bbec3a7c2e
6 changed files with 137 additions and 22 deletions

View file

@ -2,14 +2,16 @@
<resources> <resources>
<style name="Widget.Vector.Button.Text.OnPrimary.LocationLive"> <style name="Widget.Vector.Button.Text.OnPrimary.LocationLive">
<item name="android:background">?selectableItemBackground</item> <item name="android:foreground">?selectableItemBackground</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:textSize">12sp</item> <item name="android:textSize">12sp</item>
<item name="android:padding">0dp</item> <item name="android:padding">0dp</item>
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
</style> </style>
<style name="Widget.Vector.Button.Text.LocationLive"> <style name="Widget.Vector.Button.Text.LocationLive">
<item name="android:background">?selectableItemBackground</item> <item name="android:foreground">?selectableItemBackground</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:textAppearance">@style/TextAppearance.Vector.Body.Medium</item> <item name="android:textAppearance">@style/TextAppearance.Vector.Body.Medium</item>
<item name="android:textColor">?colorError</item> <item name="android:textColor">?colorError</item>
<item name="android:padding">0dp</item> <item name="android:padding">0dp</item>

View file

@ -35,6 +35,8 @@ import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners import im.vector.app.features.home.room.detail.timeline.style.granularRoundedCorners
import im.vector.app.features.location.live.LocationLiveMessageBannerView
import im.vector.app.features.location.live.LocationLiveMessageBannerViewState
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() { abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
@ -57,12 +59,17 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
renderSendState(holder.view, null) renderSendState(holder.view, null)
bindMap(holder)
bindLocationLiveBanner(holder)
}
private fun bindMap(holder: Holder) {
val location = locationUrl ?: return val location = locationUrl ?: return
val messageLayout = attributes.informationData.messageLayout val messageLayout = attributes.informationData.messageLayout
val dimensionConverter = DimensionConverter(holder.view.resources)
val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) { val imageCornerTransformation = if (messageLayout is TimelineMessageLayout.Bubble) {
messageLayout.cornersRadius.granularRoundedCorners() messageLayout.cornersRadius.granularRoundedCorners()
} else { } else {
val dimensionConverter = DimensionConverter(holder.view.resources)
RoundedCorners(dimensionConverter.dpToPx(8)) RoundedCorners(dimensionConverter.dpToPx(8))
} }
holder.staticMapImageView.updateLayoutParams { holder.staticMapImageView.updateLayoutParams {
@ -88,9 +95,8 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
dataSource: DataSource?, dataSource: DataSource?,
isFirstResource: Boolean): Boolean { isFirstResource: Boolean): Boolean {
locationPinProvider?.create(userId) { pinDrawable -> locationPinProvider?.create(userId) { pinDrawable ->
GlideApp.with(holder.staticMapPinImageView) // we are not using Glide since it does not display it correctly when there is no user photo
.load(pinDrawable) holder.staticMapPinImageView.setImageDrawable(pinDrawable)
.into(holder.staticMapPinImageView)
} }
holder.staticMapErrorTextView.isVisible = false holder.staticMapErrorTextView.isVisible = false
return false return false
@ -100,12 +106,44 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
.into(holder.staticMapImageView) .into(holder.staticMapImageView)
} }
private fun bindLocationLiveBanner(holder: Holder) {
val messageLayout = attributes.informationData.messageLayout
val viewState = if (messageLayout is TimelineMessageLayout.Bubble) {
LocationLiveMessageBannerViewState.Emitter(
remainingTimeInMillis = 4000 * 1000L,
bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
isStopButtonCenteredVertically = false
)
} else {
val dimensionConverter = DimensionConverter(holder.view.resources)
val cornerRadius = dimensionConverter.dpToPx(8).toFloat()
// LocationLiveMessageBannerViewState.Watcher(
// bottomStartCornerRadiusInDp = cornerRadius,
// bottomEndCornerRadiusInDp = cornerRadius,
// formattedLocalTimeOfEndOfLive = "12:34",
// )
LocationLiveMessageBannerViewState.Emitter(
remainingTimeInMillis = 4000 * 1000L,
bottomStartCornerRadiusInDp = cornerRadius,
bottomEndCornerRadiusInDp = cornerRadius,
isStopButtonCenteredVertically = true
)
}
holder.locationLiveMessageBanner.isVisible = true
holder.locationLiveMessageBanner.render(viewState)
// TODO create a dedicated message Item per state: Start, Location, End? Check if inheritance is possible in Epoxy model
// TODO adjust Copyright map placement
}
override fun getViewStubId() = STUB_ID override fun getViewStubId() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) { class Holder : AbsMessageItem.Holder(STUB_ID) {
val staticMapImageView by bind<ImageView>(R.id.staticMapImageView) val staticMapImageView by bind<ImageView>(R.id.staticMapImageView)
val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView) val staticMapPinImageView by bind<ImageView>(R.id.staticMapPinImageView)
val staticMapErrorTextView by bind<TextView>(R.id.staticMapErrorTextView) val staticMapErrorTextView by bind<TextView>(R.id.staticMapErrorTextView)
val locationLiveMessageBanner by bind<LocationLiveMessageBannerView>(R.id.locationLiveMessageBanner)
} }
companion object { companion object {

View file

@ -17,21 +17,42 @@
package im.vector.app.features.location.live package im.vector.app.features.location.live
import android.content.Context import android.content.Context
import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Button import android.widget.Button
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.TextUtils
import im.vector.app.databinding.ViewLocationLiveMessageBannerBinding import im.vector.app.databinding.ViewLocationLiveMessageBannerBinding
import im.vector.app.features.themes.ThemeUtils
import org.threeten.bp.Duration import org.threeten.bp.Duration
data class LocationLiveMessageBannerViewState( // TODO should it be moved to timeline.item package?
val isStopButtonVisible: Boolean, sealed class LocationLiveMessageBannerViewState(
val remainingTimeInMillis: Long open val bottomStartCornerRadiusInDp: Float,
) open val bottomEndCornerRadiusInDp: Float,
) {
data class Emitter(
override val bottomStartCornerRadiusInDp: Float,
override val bottomEndCornerRadiusInDp: Float,
val remainingTimeInMillis: Long,
val isStopButtonCenteredVertically: Boolean
) : LocationLiveMessageBannerViewState(bottomStartCornerRadiusInDp, bottomEndCornerRadiusInDp)
data class Watcher(
override val bottomStartCornerRadiusInDp: Float,
override val bottomEndCornerRadiusInDp: Float,
val formattedLocalTimeOfEndOfLive: String,
) : LocationLiveMessageBannerViewState(bottomStartCornerRadiusInDp, bottomEndCornerRadiusInDp)
}
class LocationLiveMessageBannerView @JvmOverloads constructor( class LocationLiveMessageBannerView @JvmOverloads constructor(
context: Context, context: Context,
@ -47,12 +68,57 @@ class LocationLiveMessageBannerView @JvmOverloads constructor(
val stopButton: Button val stopButton: Button
get() = binding.locationLiveMessageBannerStop get() = binding.locationLiveMessageBannerStop
private val background: ImageView
get() = binding.locationLiveMessageBannerBackground
private val title: TextView
get() = binding.locationLiveMessageBannerTitle
private val subTitle: TextView private val subTitle: TextView
get() = binding.locationLiveMessageBannerSubTitle get() = binding.locationLiveMessageBannerSubTitle
fun render(viewState: LocationLiveMessageBannerViewState) { fun render(viewState: LocationLiveMessageBannerViewState) {
stopButton.isVisible = viewState.isStopButtonVisible when (viewState) {
is LocationLiveMessageBannerViewState.Emitter -> renderEmitter(viewState)
is LocationLiveMessageBannerViewState.Watcher -> renderWatcher(viewState)
}
GlideApp.with(context)
.load(ColorDrawable(ThemeUtils.getColor(context, R.attr.colorSurface)))
.transform(GranularRoundedCorners(0f, 0f, viewState.bottomEndCornerRadiusInDp, viewState.bottomStartCornerRadiusInDp))
.into(background)
}
private fun renderEmitter(viewState: LocationLiveMessageBannerViewState.Emitter) {
stopButton.isVisible = true
title.text = context.getString(R.string.location_share_live_enabled)
val duration = Duration.ofMillis(viewState.remainingTimeInMillis.coerceAtLeast(0L)) val duration = Duration.ofMillis(viewState.remainingTimeInMillis.coerceAtLeast(0L))
subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, duration)) subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, duration))
val rootLayout: ConstraintLayout? = (binding.root as? ConstraintLayout)
rootLayout?.let { parentLayout ->
val constraintSet = ConstraintSet()
constraintSet.clone(rootLayout)
if (viewState.isStopButtonCenteredVertically) {
constraintSet.connect(
R.id.locationLiveMessageBannerStop,
ConstraintSet.BOTTOM,
R.id.locationLiveMessageBannerBackground,
ConstraintSet.BOTTOM,
0
)
} else {
constraintSet.clear(R.id.locationLiveMessageBannerStop, ConstraintSet.BOTTOM)
}
constraintSet.applyTo(parentLayout)
}
}
private fun renderWatcher(viewState: LocationLiveMessageBannerViewState.Watcher) {
stopButton.isVisible = false
title.text = context.getString(R.string.location_share_live_view)
subTitle.text = context.getString(R.string.location_share_live_until, viewState.formattedLocalTimeOfEndOfLive)
} }
} }

View file

@ -45,4 +45,12 @@
app:layout_constraintTop_toBottomOf="@id/staticMapPinImageView" app:layout_constraintTop_toBottomOf="@id/staticMapPinImageView"
tools:visibility="visible" /> tools:visibility="visible" />
<im.vector.app.features.location.live.LocationLiveMessageBannerView
android:id="@+id/locationLiveMessageBanner"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/staticMapImageView"
app:layout_constraintEnd_toEndOf="@id/staticMapImageView"
app:layout_constraintBottom_toBottomOf="@id/staticMapImageView" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -6,15 +6,16 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<View <ImageView
android:id="@+id/locationLiveMessageBannerBackground" android:id="@+id/locationLiveMessageBannerBackground"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="48dp" android:layout_height="50dp"
android:alpha="0.85" android:alpha="0.85"
android:background="?colorSurface"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent"
tools:background="?colorSurface"
tools:ignore="ContentDescription" />
<ImageView <ImageView
android:id="@+id/locationLiveMessageBannerIcon" android:id="@+id/locationLiveMessageBannerIcon"
@ -36,7 +37,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp" android:layout_marginHorizontal="8dp"
android:text="@string/location_share_live_enabled" tools:text="@string/location_share_live_enabled"
android:textColor="?colorOnSurface" android:textColor="?colorOnSurface"
app:layout_constraintBottom_toTopOf="@id/locationLiveMessageBannerSubTitle" app:layout_constraintBottom_toTopOf="@id/locationLiveMessageBannerSubTitle"
app:layout_constraintStart_toEndOf="@id/locationLiveMessageBannerIcon" app:layout_constraintStart_toEndOf="@id/locationLiveMessageBannerIcon"
@ -57,11 +58,10 @@
<Button <Button
android:id="@+id/locationLiveMessageBannerStop" android:id="@+id/locationLiveMessageBannerStop"
style="@style/Widget.Vector.Button.Text.LocationLive" style="@style/Widget.Vector.Button.Text.LocationLive"
android:layout_width="wrap_content" android:layout_width="45dp"
android:layout_height="0dp" android:layout_height="30dp"
android:layout_marginHorizontal="8dp" android:text="@string/location_share_live_stop"
android:text="@string/location_share_live_stop_long_version"
app:layout_constraintBottom_toBottomOf="@id/locationLiveMessageBannerBackground"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@id/locationLiveMessageBannerBackground"
app:layout_constraintTop_toTopOf="@id/locationLiveMessageBannerBackground" /> app:layout_constraintTop_toTopOf="@id/locationLiveMessageBannerBackground" />
</merge> </merge>

View file

@ -3018,8 +3018,9 @@
<string name="location_timeline_failed_to_load_map">Failed to load map</string> <string name="location_timeline_failed_to_load_map">Failed to load map</string>
<string name="location_share_live_enabled">Live location enabled</string> <string name="location_share_live_enabled">Live location enabled</string>
<string name="location_share_live_started">Loading live location…</string> <string name="location_share_live_started">Loading live location…</string>
<string name="location_share_live_view">View live location</string>
<string name="location_share_live_until">Live until %1$s</string>
<string name="location_share_live_stop">Stop</string> <string name="location_share_live_stop">Stop</string>
<string name="location_share_live_stop_long_version">Stop sharing</string>
<string name="location_share_live_remaining_time">%1$s left</string> <string name="location_share_live_remaining_time">%1$s left</string>
<string name="live_location_sharing_notification_title">${app_name} Live Location</string> <string name="live_location_sharing_notification_title">${app_name} Live Location</string>
<string name="live_location_sharing_notification_description">Location sharing is in progress</string> <string name="live_location_sharing_notification_description">Location sharing is in progress</string>