[SES-337] Add rounded corners to thumbnail in QuoteView (#1285)

* Add rounded corners to thumbnail in QuoteView

* Simplify ThumbnailView

* Cleanup ThumbnailView

* Removed include custom attributes

The custom attributes are not passed to the view.
I added the radius programatically instead.

* Clipping whole thumbnail view instead of just the image requests

---------

Co-authored-by: AL-Session <160798022+AL-Session@users.noreply.github.com>
Co-authored-by: ThomasSession <thomas.r@getsession.org>
pull/1528/head
Andrew 9 months ago committed by GitHub
parent 5cd2cf5cb5
commit 1d80bb0ba9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -114,7 +114,7 @@ class MediaGalleryAdapter extends StickyHeaderGridAdapter {
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment()); Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
if (slide != null) { if (slide != null) {
thumbnailView.setImageResource(glideRequests, slide, false, null); thumbnailView.setImageResource(glideRequests, slide, false);
} }
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord)); thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));

@ -104,7 +104,7 @@ class AlbumThumbnailView : RelativeLayout {
// iterate binding // iterate binding
slides.take(MAX_ALBUM_DISPLAY_SIZE).forEachIndexed { position, slide -> slides.take(MAX_ALBUM_DISPLAY_SIZE).forEachIndexed { position, slide ->
val thumbnailView = getThumbnailView(position) val thumbnailView = getThumbnailView(position)
thumbnailView.setImageResource(glideRequests, slide, isPreview = false, mms = message) thumbnailView.setImageResource(glideRequests, slide, isPreview = false)
} }
} }

@ -31,10 +31,10 @@ class LinkPreviewDraftView : LinearLayout {
// Hide the loader and show the content view // Hide the loader and show the content view
binding.linkPreviewDraftContainer.isVisible = true binding.linkPreviewDraftContainer.isVisible = true
binding.linkPreviewDraftLoader.isVisible = false binding.linkPreviewDraftLoader.isVisible = false
binding.thumbnailImageView.root.radius = toPx(4, resources) binding.thumbnailImageView.root.setRoundedCorners(toPx(4, resources))
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false, null) binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), false)
} }
binding.linkPreviewDraftTitleTextView.text = linkPreview.title binding.linkPreviewDraftTitleTextView.text = linkPreview.title
} }

@ -41,7 +41,7 @@ class LinkPreviewView : LinearLayout {
// Thumbnail // Thumbnail
if (linkPreview.getThumbnail().isPresent) { if (linkPreview.getThumbnail().isPresent) {
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false, message) binding.thumbnailImageView.root.setImageResource(glide, ImageSlide(context, linkPreview.getThumbnail().get()), isPreview = false)
binding.thumbnailImageView.root.loadIndicator.isVisible = false binding.thumbnailImageView.root.loadIndicator.isVisible = false
} }
// Title // Title

@ -108,8 +108,9 @@ class QuoteView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
attachments.thumbnailSlide != null -> { attachments.thumbnailSlide != null -> {
val slide = attachments.thumbnailSlide!! val slide = attachments.thumbnailSlide!!
// This internally fetches the thumbnail // This internally fetches the thumbnail
binding.quoteViewAttachmentThumbnailImageView.root.radius = toPx(4, resources) binding.quoteViewAttachmentThumbnailImageView
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false, null) .root.setRoundedCorners(toPx(4, resources))
binding.quoteViewAttachmentThumbnailImageView.root.setImageResource(glide, slide, false)
binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true binding.quoteViewAttachmentThumbnailImageView.root.isVisible = true
binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image) binding.quoteViewBodyTextView.text = if (MediaUtil.isVideo(slide.asAttachment())) resources.getString(R.string.Slide_video) else resources.getString(R.string.Slide_image)
} }

@ -2,10 +2,13 @@ package org.thoughtcrime.securesms.conversation.v2.utilities
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Outline
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewOutlineProvider
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
@ -21,18 +24,17 @@ import org.session.libsignal.utilities.ListenableFuture
import org.session.libsignal.utilities.SettableFuture import org.session.libsignal.utilities.SettableFuture
import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget import org.thoughtcrime.securesms.components.GlideBitmapListeningTarget
import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri
import org.thoughtcrime.securesms.mms.GlideRequest import org.thoughtcrime.securesms.mms.GlideRequest
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
import kotlin.Boolean
import kotlin.Int
import kotlin.getValue
import kotlin.lazy
import kotlin.let
open class ThumbnailView: FrameLayout { open class ThumbnailView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
companion object { companion object {
private const val WIDTH = 0 private const val WIDTH = 0
private const val HEIGHT = 1 private const val HEIGHT = 1
@ -41,30 +43,29 @@ open class ThumbnailView: FrameLayout {
private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) } private val binding: ThumbnailViewBinding by lazy { ThumbnailViewBinding.bind(this) }
// region Lifecycle // region Lifecycle
constructor(context: Context) : super(context) { initialize(null) }
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { initialize(attrs) }
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { initialize(attrs) }
val loadIndicator: View by lazy { binding.thumbnailLoadIndicator } val loadIndicator: View by lazy { binding.thumbnailLoadIndicator }
private val dimensDelegate = ThumbnailDimensDelegate() private val dimensDelegate = ThumbnailDimensDelegate()
private var slide: Slide? = null private var slide: Slide? = null
var radius: Int = 0
private fun initialize(attrs: AttributeSet?) {
if (attrs != null) {
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0)
dimensDelegate.setBounds(typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0),
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0))
radius = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0) init {
attrs?.let { context.theme.obtainStyledAttributes(it, R.styleable.ThumbnailView, 0, 0) }
typedArray.recycle() ?.apply {
} dimensDelegate.setBounds(
getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0),
getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0),
getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0),
getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0)
)
setRoundedCorners(
getDimensionPixelSize(R.styleable.ThumbnailView_thumbnail_radius, 0)
)
recycle()
}
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
@ -84,114 +85,118 @@ open class ThumbnailView: FrameLayout {
private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0) private fun getDefaultWidth() = maxOf(layoutParams?.width ?: 0, 0)
private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0) private fun getDefaultHeight() = maxOf(layoutParams?.height ?: 0, 0)
// endregion // endregion
// region Interaction // region Interaction
fun setImageResource(glide: GlideRequests, slide: Slide, isPreview: Boolean, mms: MmsMessageRecord?): ListenableFuture<Boolean> { fun setRoundedCorners(radius: Int){
return setImageResource(glide, slide, isPreview, 0, 0, mms) // create an outline provider and clip the whole view to that shape
} // that way we can round the image and the background ( and any other artifacts that the view may contain )
val mOutlineProvider = object : ViewOutlineProvider() {
fun setImageResource(glide: GlideRequests, slide: Slide, override fun getOutline(view: View, outline: Outline) {
isPreview: Boolean, naturalWidth: Int, // all corners
naturalHeight: Int, mms: MmsMessageRecord?): ListenableFuture<Boolean> { outline.setRoundRect(0, 0, view.width, view.height, radius.toFloat())
}
}
val currentSlide = this.slide outlineProvider = mOutlineProvider
clipToOutline = true
}
fun setImageResource(
glide: GlideRequests,
slide: Slide,
isPreview: Boolean
): ListenableFuture<Boolean> = setImageResource(glide, slide, isPreview, 0, 0)
fun setImageResource(
glide: GlideRequests, slide: Slide,
isPreview: Boolean, naturalWidth: Int,
naturalHeight: Int
): ListenableFuture<Boolean> {
binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() && binding.playOverlay.isVisible = (slide.thumbnailUri != null && slide.hasPlayOverlay() &&
(slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview)) (slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_DONE || isPreview))
if (equals(currentSlide, slide)) { if (equals(this.slide, slide)) {
// don't re-load slide // don't re-load slide
return SettableFuture(false) return SettableFuture(false)
} }
if (currentSlide != null && currentSlide.fastPreflightId != null && currentSlide.fastPreflightId == slide.fastPreflightId) {
// not reloading slide for fast preflight
this.slide = slide
}
this.slide = slide this.slide = slide
binding.thumbnailLoadIndicator.isVisible = slide.isInProgress binding.thumbnailLoadIndicator.isVisible = slide.isInProgress
binding.thumbnailDownloadIcon.isVisible = slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED binding.thumbnailDownloadIcon.isVisible =
slide.transferState == AttachmentTransferProgress.TRANSFER_PROGRESS_FAILED
dimensDelegate.setDimens(naturalWidth, naturalHeight) dimensDelegate.setDimens(naturalWidth, naturalHeight)
invalidate() invalidate()
val result = SettableFuture<Boolean>() return SettableFuture<Boolean>().also {
when {
when { slide.thumbnailUri != null -> {
slide.thumbnailUri != null -> { buildThumbnailGlideRequest(glide, slide).into(
buildThumbnailGlideRequest(glide, slide).into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, result)) GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, it)
} )
slide.hasPlaceholder() -> { }
buildPlaceholderGlideRequest(glide, slide).into(GlideBitmapListeningTarget(binding.thumbnailImage, null, result)) slide.hasPlaceholder() -> {
} buildPlaceholderGlideRequest(glide, slide).into(
else -> { GlideBitmapListeningTarget(binding.thumbnailImage, null, it)
glide.clear(binding.thumbnailImage) )
result.set(false) }
else -> {
glide.clear(binding.thumbnailImage)
it.set(false)
}
} }
} }
return result
} }
fun buildThumbnailGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Drawable> { private fun buildThumbnailGlideRequest(
glide: GlideRequests,
val dimens = dimensDelegate.resourceSize() slide: Slide
): GlideRequest<Drawable> = glide.load(DecryptableUri(slide.thumbnailUri!!))
val request = glide.load(DecryptableUri(slide.thumbnailUri!!)) .diskCacheStrategy(DiskCacheStrategy.NONE)
.diskCacheStrategy(DiskCacheStrategy.NONE) .overrideDimensions()
.let { request -> .transition(DrawableTransitionOptions.withCrossFade())
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) { .transform(CenterCrop())
request.override(getDefaultWidth(), getDefaultHeight()) .missingThumbnailPicture(slide.isInProgress)
} else {
request.override(dimens[WIDTH], dimens[HEIGHT]) private fun buildPlaceholderGlideRequest(
} glide: GlideRequests,
} slide: Slide
.transition(DrawableTransitionOptions.withCrossFade()) ): GlideRequest<Bitmap> = glide.asBitmap()
.centerCrop() .load(slide.getPlaceholderRes(context.theme))
.diskCacheStrategy(DiskCacheStrategy.NONE)
return if (slide.isInProgress) request else request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)) .overrideDimensions()
} .fitCenter()
fun buildPlaceholderGlideRequest(glide: GlideRequests, slide: Slide): GlideRequest<Bitmap> {
val dimens = dimensDelegate.resourceSize()
return glide.asBitmap()
.load(slide.getPlaceholderRes(context.theme))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.let { request ->
if (dimens[WIDTH] == 0 || dimens[HEIGHT] == 0) {
request.override(getDefaultWidth(), getDefaultHeight())
} else {
request.override(dimens[WIDTH], dimens[HEIGHT])
}
}
.fitCenter()
}
open fun clear(glideRequests: GlideRequests) { open fun clear(glideRequests: GlideRequests) {
glideRequests.clear(binding.thumbnailImage) glideRequests.clear(binding.thumbnailImage)
slide = null slide = null
} }
fun setImageResource(glideRequests: GlideRequests, uri: Uri): ListenableFuture<Boolean> { fun setImageResource(
val future = SettableFuture<Boolean>() glideRequests: GlideRequests,
uri: Uri
var request: GlideRequest<Drawable> = glideRequests.load(DecryptableUri(uri)) ): ListenableFuture<Boolean> = glideRequests.load(DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(DrawableTransitionOptions.withCrossFade()) .transition(DrawableTransitionOptions.withCrossFade())
.transform(CenterCrop())
request = if (radius > 0) { .intoDrawableTargetAsFuture()
request.transforms(CenterCrop(), RoundedCorners(radius))
} else { private fun GlideRequest<Drawable>.intoDrawableTargetAsFuture() =
request.transforms(CenterCrop()) SettableFuture<Boolean>().also {
binding.run {
GlideDrawableListeningTarget(thumbnailImage, thumbnailLoadIndicator, it)
}.let { into(it) }
} }
request.into(GlideDrawableListeningTarget(binding.thumbnailImage, binding.thumbnailLoadIndicator, future)) private fun <T> GlideRequest<T>.overrideDimensions() =
dimensDelegate.resourceSize().takeIf { 0 !in it }
?.let { override(it[WIDTH], it[HEIGHT]) }
?: override(getDefaultWidth(), getDefaultHeight())
}
return future private fun <T> GlideRequest<T>.missingThumbnailPicture(
} inProgress: Boolean
} ) = takeIf { inProgress } ?: apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture))

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.mediapreview; package org.thoughtcrime.securesms.mediapreview;
import static org.thoughtcrime.securesms.util.GeneralUtilitiesKt.toPx;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -151,6 +153,8 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
{ {
image.setImageResource(glideRequests, media.getUri()); image.setImageResource(glideRequests, media.getUri());
image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive)); image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive));
// set the rounded corners
image.setRoundedCorners(toPx(5, image.getResources()));
outline.setVisibility(isActive ? View.VISIBLE : View.GONE); outline.setVisibility(isActive ? View.VISIBLE : View.GONE);

@ -9,11 +9,6 @@
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"/>
app:minWidth="@dimen/media_bubble_min_width"
app:maxWidth="@dimen/media_bubble_max_width"
app:minHeight="@dimen/media_bubble_min_height"
app:maxHeight="@dimen/media_bubble_max_height"
app:thumbnail_radius="1dp"/>
</FrameLayout> </FrameLayout>

@ -10,14 +10,12 @@
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_2_cell_width" android:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height" android:layout_height="@dimen/album_2_total_height"/>
app:thumbnail_radius="0dp"/>
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_2_cell_width" android:layout_width="@dimen/album_2_cell_width"
android:layout_height="@dimen/album_2_total_height" android:layout_height="@dimen/album_2_total_height"
android:layout_gravity="end" android:layout_gravity="end"/>
app:thumbnail_radius="0dp"/>
</FrameLayout> </FrameLayout>

@ -9,15 +9,13 @@
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_1" android:id="@+id/album_cell_1"
android:layout_width="@dimen/album_3_cell_width_big" android:layout_width="@dimen/album_3_cell_width_big"
android:layout_height="@dimen/album_3_total_height" android:layout_height="@dimen/album_3_total_height"/>
app:thumbnail_radius="0dp"/>
<include layout="@layout/thumbnail_view" <include layout="@layout/thumbnail_view"
android:id="@+id/album_cell_2" android:id="@+id/album_cell_2"
android:layout_width="@dimen/album_3_cell_size_small" android:layout_width="@dimen/album_3_cell_size_small"
android:layout_height="@dimen/album_3_cell_size_small" android:layout_height="@dimen/album_3_cell_size_small"
android:layout_gravity="end|top" android:layout_gravity="end|top"/>
app:thumbnail_radius="0dp"/>
<FrameLayout <FrameLayout
@ -29,8 +27,7 @@
android:id="@+id/album_cell_3" android:id="@+id/album_cell_3"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_gravity="center_horizontal|bottom" android:layout_gravity="center_horizontal|bottom"/>
app:thumbnail_radius="0dp"/>
<TextView <TextView
tools:visibility="visible" tools:visibility="visible"

@ -12,9 +12,7 @@
android:id="@+id/rail_item_image" android:id="@+id/rail_item_image"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:layout_gravity="center" android:layout_gravity="center"/>
android:background="@drawable/mediarail_media_outline"
app:thumbnail_radius="5dp"/>
<ImageView <ImageView
android:id="@+id/rail_item_outline" android:id="@+id/rail_item_outline"

Loading…
Cancel
Save